Author Topic: Reorder Attributes by Swapping ObjectIds (Proof of concept with WPF dialog)  (Read 2395 times)

0 Members and 1 Guest are viewing this topic.

kaefer

  • Guest
Greetings!

Ever since I read Kean' s blog http://through-the-interface.typepad.com/through_the_interface/2010/07/swapping-autocad-block-attribute-order-using-net.html about the DBObject.SwapIdWith method, I wanted to use it for the full blown problem of reordering AttributeDefinitions/AttributeReferences in the wild.

Now there are still a couple of minor issues with this, but it looks viable. A WPF dialog inside an ElementHost inside a Form shown modally by AutoCAD's Application provides the GUI, the swapping is handled by an array paralleling the swapped ObjectIds. At least it wouldn't be necessary to delete all Attributes and recreate them from scratch in the correct order.

Here it goes, and alas, it's in F#.
Code: [Select]
// Needs references to the usual WPF libraries and also to WindowsFormsIntegration.dll

open System.Windows.Forms
open System.Windows.Controls
open System.Windows.Input

open Autodesk.AutoCAD.ApplicationServices
open Autodesk.AutoCAD.DatabaseServices
open Autodesk.AutoCAD.EditorInput
open Autodesk.AutoCAD.Runtime

type acApp = Autodesk.AutoCAD.ApplicationServices.Application

// Record definition for ListBox properties
type ItemDataType = { Index: int; Value: string }

// Record definition for Array/ObjectId swapping operations
type ArrayDataType = { Tag: string; DBObject: DBObject }

// WPF Dialog definition
let xaml = @"
<UserControl
    xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
    Height='480' Width='300'>
    <UserControl.Resources>
        <Style x:Key='MyItem' TargetType='{x:Type ListBox}'>
            <Setter Property='ItemTemplate'>
                <Setter.Value>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width='40' />
                                <ColumnDefinition Width='*' />
                            </Grid.ColumnDefinitions>
                            <Label Grid.Column='0' Content='{Binding Path=Index}'/>
                            <Label Grid.Column='1' Content='{Binding Path=Value}'/>
                        </Grid>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property='ItemsPanel'>
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <VirtualizingStackPanel IsItemsHost='True' />
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height='20' />
            <RowDefinition Height='*' />
        </Grid.RowDefinitions>
        <StackPanel Orientation='Horizontal' Grid.Row='0'>
            <Button Name='buttonFirst' Content='Top' Width='55'/>
            <Button Name='buttonUp' Content='Up' Width='55'/>
            <Button Name='buttonDown' Content='Down' Width='55'/>
            <Button Name='buttonLast' Content='Bottom' Width='55'/>
            <Button Name='buttonApply' Content='Apply' Width='55' Margin='20,0,0,0' />
        </StackPanel>       
        <ListBox Name='listBox' Style='{StaticResource MyItem}' Grid.Row='1'
                    HorizontalContentAlignment='Stretch'
                    ScrollViewer.HorizontalScrollBarVisibility='Disabled' />
    </Grid>
</UserControl>"

// Populates the ListBox
let atts = new System.Collections.ObjectModel.ObservableCollection<_>()

let (?) (this : Control) (prop : string) : 'T = // '
    this.FindName(prop) :?> 'T
let (+=) (e: System.IObservable<_>) f =
    Observable.add f e

// WPF Controls setup
let uc = System.Windows.Markup.XamlReader.Parse xaml :?> UserControl
let listbox: ListBox = uc?listBox
let buttonFirst: Button = uc?buttonFirst
let buttonUp: Button = uc?buttonUp
let buttonDown: Button = uc?buttonDown
let buttonLast: Button = uc?buttonLast
let buttonApply: Button = uc?buttonApply
   
listbox.ItemsSource <- atts

// Forms setup, hosting the WPF Dialog
let form = new Form()
new System.Windows.Forms.Integration.ElementHost(
    AutoSize = true,
    Dock = DockStyle.Fill,
    Child = uc )
|> form.Controls.Add

// Helper for control key modifier
let isCtrlDown() =
    Keyboard.Modifiers &&& ModifierKeys.Control = ModifierKeys.Control

// Helper for ListBox items manipulation
let update oldpos newpos =
    listbox.UnselectAll()
    let tmp = atts.[oldpos]
    atts.RemoveAt oldpos
    atts.Insert(newpos, tmp)
    listbox.SelectedIndex <- newpos

// Functions bound to buttons and/or keys
let first() =
    if listbox.SelectedIndex > 0 then
        update listbox.SelectedIndex 0
let up() =
    if listbox.SelectedIndex > 0 then
        update listbox.SelectedIndex (listbox.SelectedIndex - 1)
let down() =
    if  listbox.SelectedIndex >= 0 &&
        listbox.SelectedIndex < atts.Count - 1 then
        update listbox.SelectedIndex (listbox.SelectedIndex + 1)
let last() =
    if  listbox.SelectedIndex >= 0 &&
        listbox.SelectedIndex < atts.Count - 1 then
        update listbox.SelectedIndex (atts.Count - 1)
let exitnow() =
    form.DialogResult <- DialogResult.Cancel
    form.Close()
let apply() =
    form.DialogResult <- DialogResult.OK
    form.Close()

// The actual plumbing for event handling
buttonFirst.Click += fun _ -> first()
buttonUp.Click += fun _ -> up()
buttonDown.Click += fun _ -> down()
buttonLast.Click += fun _ -> last()
buttonApply.Click += fun _ -> apply()

listbox.KeyUp +=
    fun e ->
        match e.Key with
        | Key.Escape ->                 e.Handled <- true; exitnow()
        | Key.PageUp | Key.F ->         e.Handled <- true; first()
        | Key.Up when isCtrlDown() ->   e.Handled <- true; up()
        | Key.U ->                      e.Handled <- true; up()
        | Key.Down when isCtrlDown() -> e.Handled <- true; down()
        | Key.D->                       e.Handled <- true; down()
        | Key.PageDown | Key.L ->       e.Handled <- true; last()
        | Key.A ->                      e.Handled <- true; apply()
        | _ -> ()

// Swap two array elements
let swap (a:_[]) i j = let tmp = a.[i] in a.[i] <- a.[j]; a.[j] <- tmp

// AutoCAD-faced functionality: Reorder attribute definitions and references
// for the supplied Block Name by Tag according to ordering array
let swapAttributes (blkName: string) (attorder: _ []) =
    let doc = acApp.DocumentManager.MdiActiveDocument
    let db = doc.Database
    let ed = doc.Editor
 
    // Keep count of what we're doing
    let (attDefCnt, blkDefCnt, attRefCnt, blkRefCnt) =
        ref 0, ref 0, ref 0, ref 0

    // The actual attribute reordering happens here
    let reOrder (ctr: _ ref) (ads: _ [])  =
        for (n, x) in ads |> Array.mapi (fun l m -> l, m) do
            // Find the first occurrence of the current tag in the working array
            let pos0 = Array.tryFindIndex (fun t -> t.Tag = x.Tag) ads
            // Find the position of the current tag in the target ordering array
            let pos1 = Array.tryFindIndex ((=) x.Tag) attorder
            match pos0, pos1 with
            | None, _ -> ()                // Can't happen
            | _, None -> ()                // No target sorting for this tag
            | _, Some i' when n = i' -> () // Already correctly sorted
            | Some(i), Some i' ->          // Swap array elements and ObjectIds
                swap ads i i'
                ads.[i].DBObject.SwapIdWith(
                    ads.[i'].DBObject.ObjectId, true, true )
                incr ctr

    // Now loop through the BlockTableRecord and the AttributeCollections of
    // all BlockReferences for this block name
    use tr = db.TransactionManager.StartTransaction()
     
    let bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) :?> BlockTable
    if bt.Has blkName then
        let btr = tr.GetObject(bt.[blkName], OpenMode.ForRead) :?> BlockTableRecord

        // If the block definition has attribute definitions...
        if btr.HasAttributeDefinitions then
            btr
            |> Seq.cast<ObjectId>
            |> Seq.filter
                (fun oid ->
                    typeof<AttributeDefinition>
                    |> RXClass.GetClass
                    |> oid.ObjectClass.IsDerivedFrom )
            |> Seq.map
                (fun oid ->
                    let ad = tr.GetObject(oid, OpenMode.ForWrite)
                    { Tag = (ad :?> AttributeDefinition).Tag; DBObject = ad } )
            |> Array.ofSeq
            |> reOrder attDefCnt
           
            incr blkDefCnt

        for broid in btr.GetBlockReferenceIds(true, true) do
            let br = tr.GetObject(broid, OpenMode.ForRead) :?> BlockReference
            if br.AttributeCollection.Count > 1 then
                br.AttributeCollection
                |> Seq.cast<ObjectId>
                |> Seq.map
                    (fun oid ->
                        let ar = tr.GetObject(oid, OpenMode.ForWrite)
                        {   Tag = (ar :?> AttributeReference).Tag
                            DBObject = ar } )
                |> Array.ofSeq
                |> reOrder attRefCnt
               
                incr blkRefCnt
   
    tr.Commit()

    // Now say what we did
    let writeMessage number substantive verb =
        ed.WriteMessage(
            "\n{0} {1}{2} {3} ", number, substantive,
            (if number = 1 then "" else "s"), verb )
    writeMessage !attDefCnt "Attribute Definition" "reordered"
    writeMessage !blkDefCnt "Block Definition" "changed"
    writeMessage !attRefCnt "Attribute Reference" "reordered"
    writeMessage !blkRefCnt "Block Reference" "changed"

// Here's the command: Select a Block Reference, show the dialog filled with its
// Attribute Refences and if applied, use the sort order from its ListBox
[<CommandMethod "MyReOrder">]
let myReOrder() =
    let doc = acApp.DocumentManager.MdiActiveDocument
    let ed = doc.Editor
    let peo = new PromptEntityOptions("\nSelect BlockReference: ")
    peo.SetRejectMessage "\nHas to be BlockReference. "
    peo.AddAllowedClass(typeof<BlockReference>, true)
    let per = ed.GetEntity peo
    if per.Status = PromptStatus.OK then
        atts.Clear()

        use tr = doc.TransactionManager.StartTransaction()
        let br = tr.GetObject(per.ObjectId, OpenMode.ForRead) :?> BlockReference
        let blkName = br.Name

        br.AttributeCollection
        |> Seq.cast<ObjectId>
        |> Seq.iteri
            (fun i oid ->
                let ar = tr.GetObject(oid, OpenMode.ForRead)
                atts.Add{ Index = i; Value = (ar :?> AttributeReference).Tag } )

        tr.Commit()

        if acApp.ShowModalDialog(form) = DialogResult.OK then
            atts
            |> Seq.map (fun x -> x.Value)
            |> Array.ofSeq
            |> swapAttributes blkName
Any thoughts? Thorsten