Author Topic: [F#] Why do I need to use a nested transaction ?  (Read 2929 times)

0 Members and 1 Guest are viewing this topic.

gile

  • Gator
  • Posts: 2507
  • Marseille, France
[F#] Why do I need to use a nested transaction ?
« on: March 23, 2011, 06:05:56 PM »
Hi,

Trying to write a little function to return all block reference ObjectIds for a specific name I began this way:

Code: [Select]
let GetBlockReferences (db : Database) (bName : string) =
    use tr = db.TransactionManager.StartTransaction()
    let bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) :?> BlockTable
    if bt.Has(bName) then
        let btr = tr.GetObject(bt.[bName], OpenMode.ForRead) :?> BlockTableRecord
        btr.GetBlockReferenceIds(true, false)
        |> Seq.cast<_>
        |> Seq.filter (fun id ->
            let br = tr.GetObject(id, OpenMode.ForRead)
            let owner = tr.GetObject(br.OwnerId, OpenMode.ForRead) :?> BlockTableRecord
            owner.IsLayout)
    else Seq.empty

But:
let br = tr.GetObject(id, OpenMode.ForRead)
within the the anonymous function for Seq.filter made AutoCAD crash (fatal error).
I can solve it using a nested transaction but can't understand why it's needed.

This code works:

Code: [Select]
let GetBlockReferences (db : Database) (bName : string) =
    use tr = db.TransactionManager.StartTransaction()
    let bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) :?> BlockTable
    if bt.Has(bName) then
        let btr = tr.GetObject(bt.[bName], OpenMode.ForRead) :?> BlockTableRecord
        btr.GetBlockReferenceIds(true, false)
        |> Seq.cast<_>
        |> Seq.filter (fun id ->
            use trx = db.TransactionManager.StartTransaction()
            let br = trx.GetObject(id, OpenMode.ForRead)
            let owner = trx.GetObject(br.OwnerId, OpenMode.ForRead) :?> BlockTableRecord
            owner.IsLayout)
    else Seq.empty

But I don't need to use a nested transaction in the Seq.choose function of this test command:

Code: [Select]
[<CommandMethod("test")>]
let test() =
    let doc = AcAp.DocumentManager.MdiActiveDocument
    let db = doc.Database
    let ed = doc.Editor
    use tr = db.TransactionManager.StartTransaction()
    tr.GetObject(db.BlockTableId, OpenMode.ForRead) :?> BlockTable
    |> Seq.cast<_>
    |> Seq.choose (fun id ->
        let btr = tr.GetObject(id, OpenMode.ForRead) :?> BlockTableRecord
        let name = btr.Name
        if name.ToUpper().Contains("_SPACE") then None else Some name)
    |> Seq.iter (fun n -> ed.WriteMessage("\n{0} = {1}", n, GetBlockReferences db n |> Seq.length))
« Last Edit: March 23, 2011, 06:22:52 PM by gile »
Speaking English as a French Frog

Jeff H

  • Needs a day job
  • Posts: 6144
Re: [F#] Why do I need to use a nested transaction ?
« Reply #1 on: March 23, 2011, 06:21:54 PM »
Hey gile, you know more than I do about this, but
could it possbly have something to do with the that sequences are evaluated lazily, and Dispose is being called.

gile

  • Gator
  • Posts: 2507
  • Marseille, France
Re: [F#] Why do I need to use a nested transaction ?
« Reply #2 on: March 23, 2011, 06:24:32 PM »
I thaught about this, but it works fine in the test command...
Speaking English as a French Frog

Jeff H

  • Needs a day job
  • Posts: 6144
Re: [F#] Why do I need to use a nested transaction ?
« Reply #3 on: March 23, 2011, 06:28:38 PM »
From Expert F# 2.0 if it helps
Quote
It’s common to implement computations that access external resources such as databases but that
return their results on demand. But this raises a difficulty: how do you manage the lifetime of the
resources for the underlying operating system connections? One solution to this is captured by use
bindings in sequence expressions:
   • When a use binding occurs in a sequence expression, the resource is initialized
     each time a client enumerates the sequence.
  • The connection is closed when the client disposes of the enumerator.

For example, consider the following function that creates a sequence expression that reads the
first two lines of a file on demand:

open System.IO
let firstTwoLines(file) =
    seq { use s = File.OpenText(file)
             yield s.ReadLine()
             yield s.ReadLine() }

Let’s now create a file and a sequence that reads the first two lines of the file on demand:

> File.WriteAllLines("test1.txt", [| "Es kommt ein Schiff";
"A ship is coming" |]);;
val it : unit = ()

> let seq = firstTwoLines("test1.txt");;
val seq : seq<string>


At this point, the file hasn’t yet been opened, and no lines have been read from the file. If you now
iterate the sequence expression, the file is opened, the first two lines are read, and the results are
consumed from the sequence and printed. Most important, the file has now also been closed, because
the Seq.iter aggregate operator is careful to dispose of the underlying enumerator it uses for the
sequence, which in turn disposes of the file handle generated by File.OpenText:

> seq |> Seq.iter (printfn "line = '%s'");;
line = 'Es kommt ein Schiff'
line = A ship is coming'


Chapter 9 covers sequence expressions and the more general mechanism of workflows in more
detail.

Here is something from a pdf I got if it helps

gile

  • Gator
  • Posts: 2507
  • Marseille, France
Re: [F#] Why do I need to use a nested transaction ?
« Reply #4 on: March 23, 2011, 06:32:47 PM »
I think you're right with the lazyness issue.
Your reply gave me an idea: 'commit' the sequence with Seq.toArray and it seems to work.

Code: [Select]
let GetBlockReferences (db : Database) (bName : string) =
    use tr = db.TransactionManager.StartTransaction()
    let bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) :?> BlockTable
    if bt.Has(bName) then
        let btr = tr.GetObject(bt.[bName], OpenMode.ForRead) :?> BlockTableRecord
        btr.GetBlockReferenceIds(true, false)
        |> Seq.cast<_>
        |> Seq.filter (fun id ->
            let br = tr.GetObject(id, OpenMode.ForRead)
            let owner = tr.GetObject(br.OwnerId, OpenMode.ForRead) :?> BlockTableRecord
            owner.IsLayout)
        |> Seq.toArray
    else Array.empty
Speaking English as a French Frog

gile

  • Gator
  • Posts: 2507
  • Marseille, France
Re: [F#] Why do I need to use a nested transaction ?
« Reply #5 on: March 24, 2011, 03:36:49 AM »
So, here's the 'final' function.
It returns an array of all ObjectIds of block references inserted in layouts for the specific name.
If the block is a dynamic block, the anonymous block references creatted from this named block are returned too.

Code: [Select]
let GetBlockReferences (db : Database) (bName : string) =
    use tr = db.TransactionManager.StartTransaction()
    let bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) :?> BlockTable
    if bt.Has(bName) then
        let btr = tr.GetObject(bt.[bName], OpenMode.ForRead) :?> BlockTableRecord
        btr.GetAnonymousBlockIds()
        |> Seq.cast<_>
        |> Seq.map (fun id -> tr.GetObject(id, OpenMode.ForRead) :?> BlockTableRecord)
        |> Seq.append (Seq.singleton btr)
        |> Seq.collect (fun b ->
            b.GetBlockReferenceIds(true, false)
            |> Seq.cast<_>
            |> Seq.filter (fun id ->
                let br = tr.GetObject(id, OpenMode.ForRead)
                let owner = tr.GetObject(br.OwnerId, OpenMode.ForRead) :?> BlockTableRecord
                owner.IsLayout))
        |> Seq.toArray
    else Array.empty

A test command which select/grip all references for the specified name:

Code: [Select]
[<CommandMethod("test")>]
let test() =
    let doc = AcAp.DocumentManager.MdiActiveDocument
    let ed = doc.Editor
    let pr = ed.GetString("\nBlock name:")
    if pr.Status = PromptStatus.OK then
        ed.SetImpliedSelection(GetBlockReferences doc.Database pr.StringResult)
« Last Edit: March 24, 2011, 04:04:44 AM by gile »
Speaking English as a French Frog