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#.
// 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