Tuesday, September 27, 2022

Selecting a Segment of a Polyline Visually

By arriving at New Orleans for AU2022 early I planned to explore the The Big Easy city for its unique scene and culture. But the unbearable heat (at least to me, as Canadian from far up north) forced me staying air-conditioned indoor more than I wanted. As a runner, I also planned some morning runs whenever I am in a different city, but I decided I'd better not run here to avoid unexpected heat stroke. Thus I end up having a bit more time to write some code for a question I saw in the .NET discussion forum here. While the answer to that question is fairly simple, but it still needs to be better explained with code, thus this post.

The question asked there actually involves 2 different programming tasks: identifying the selected segment of a polyline (e.g. the user should select a polyline, and the code then determine which segment of the polyline is actually clicked); and showing a visual hint to the user to indicate which segment is selected. With these 2 separate coding tasks in mind, the code then can be structured easily.

First, for identifying selected segment, I create the class PolylineSegmentSelector, which is really simple - if the user selected a polyline, based on where the picked point is, the segment's index is calculated:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using System;
using CadDb = Autodesk.AutoCAD.DatabaseServices;
 
namespace MiscTest
{
    public class PolylineSegmentSelector
    {
        private Document _dwg;
        private Editor _ed;
        private Database _db;
 
        public ObjectId SelectedPolyLine { private setget; } = ObjectId.Null;
        public int SelectedSegment { private setget; } = -1;
 
        public bool SelectSegment(Document dwg)
        {
            _dwg = dwg;
            _ed = dwg.Editor;
            _db = dwg.Database;
 
            SelectedPolyLine = ObjectId.Null;
            SelectedSegment = -1;
 
            var opt = new PromptEntityOptions(
                "\nSelect target segment of a polyline:");
            opt.SetRejectMessage("\nInvalid selection: not a polyline.");
            opt.AddAllowedClass(typeof(CadDb.Polyline), true);
            var res = _ed.GetEntity(opt);
            if (res.Status != PromptStatus.OK) return false;
 
            SelectedPolyLine = res.ObjectId;
            SelectedSegment = FindSelectedSegment(res.ObjectId, res.PickedPoint);
 
            return true;
        }
 
        private int FindSelectedSegment(ObjectId polyId, Point3d pickedPt)
        {
            int index = -1;
            using (var tran = 
                polyId.Database.TransactionManager.StartOpenCloseTransaction())
            {
                var poly = (CadDb.Polyline)tran.GetObject(polyId, OpenMode.ForRead);
                var ptOnPoly = poly.GetClosestPointTo(pickedPt, false);
                index = Convert.ToInt32(Math.Floor(poly.GetParameterAtPoint(ptOnPoly)));
                tran.Commit();
            }
 
            return index;
        }
    }
}

Then, the class PolylineSegmentHighlighter, which presents a temporary visual hint withTransient Graphics:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.GraphicsInterface;
using System;
using System.Collections.Generic;
using CadDb = Autodesk.AutoCAD.DatabaseServices;
 
namespace MiscTest
{
    public class PolylineSegmentHighlighter : IDisposable
    {
        private class SegmentGhost
        {
            public ObjectId PolylineId { getset; } = ObjectId.Null;
            public int SegmentIndex { getset; } = 0;
            public int ColorIndex { getset; } = 0;
            public Drawable Ghost { getset; } = null;
        }
 
        private readonly List<SegmentGhost> _ghosts = new List<SegmentGhost>();
        private TransientManager _tsManager = TransientManager.CurrentTransientManager;
 
        public void SetSegmentHighlight(ObjectId polyIdint segIndexint colorIndex)
        {
            RemoveSegmentHighlight(polyId, segIndex);
            var newGhost = CreateHighlightGhost(polyId, segIndex, colorIndex);
            _ghosts.Add(newGhost);
            _tsManager.AddTransient(
                newGhost.Ghost, 
                TransientDrawingMode.DirectTopmost, 
                128, 
                new Autodesk.AutoCAD.Geometry.IntegerCollection());
        }
 
        public void RemoveSegmentHighlight(ObjectId polyIdint segIndex)
        {
            foreach (var ghost in _ghosts)
            {
                if (ghost.PolylineId == polyId && 
                    ghost.SegmentIndex == segIndex &&
                    ghost.Ghost!=null)
                {
                    _tsManager.EraseTransient(
                        ghost.Ghost, new Autodesk.AutoCAD.Geometry.IntegerCollection());
                    _ghosts.Remove(ghost);
                    ghost.Ghost.Dispose();
                    break;
                }
            }
        }
 
        public void Dispose()
        {
            foreach (var ghost in _ghosts)
            {
                if (ghost.Ghost != null)
                {
                    _tsManager.EraseTransient(
                        ghost.Ghost, new Autodesk.AutoCAD.Geometry.IntegerCollection());
                    ghost.Ghost.Dispose();
                }
            }
            _ghosts.Clear();
        }
 
        private SegmentGhost CreateHighlightGhost(ObjectId polyIdint segIndexint colorIndex)
        {
            Entity ghost = null;
            using (var tran = 
                polyId.Database.TransactionManager.StartOpenCloseTransaction())
            {
                var poly = (CadDb.Polyline)tran.GetObject(polyId, OpenMode.ForRead);
                if (poly.NumberOfVertices > 2)
                {
                    DBObjectCollection ents = new DBObjectCollection();
                    poly.Explode(ents); 
 
                    for (int i = 0; i < ents.Count; i++)
                    {
                        if (i == segIndex)
                        {
                            ghost = ents[i] as Entity;
                        }
                        else
                        {
                            ents[i].Dispose();
                        }
                    }
                }
                else
                {
                    ghost = poly.Clone() as Entity;
                }
                tran.Commit();
            }
 
            if (ghost != null)
            {
                ghost.ColorIndex = colorIndex;
                return new SegmentGhost() 
                { 
                    PolylineId=polyId, 
                    SegmentIndex=segIndex, 
                    ColorIndex = colorIndex, 
                    Ghost=ghost 
                };
            }
            else
            {
                return null;
            }
        }
    }
}

Following CommandMethod demonstrates how to use these 2 classes:

[CommandMethod("SelectPolySeg")]
public static void SelectPolylineSegment()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var selectedPolySegs = new List<(ObjectId polyId, int segIndex)>();
 
    var selector = new PolylineSegmentSelector();
    using (var highlighter = new PolylineSegmentHighlighter())
    {
        int color = 1;
        while(selector.SelectSegment(dwg))
        {
            var selectedPoly = selector.SelectedPolyLine;
            var segIndex = selector.SelectedSegment;
 
            selectedPolySegs.Add((selectedPoly, segIndex));
 
            highlighter.SetSegmentHighlight(selectedPoly, segIndex, color);
            CadApp.UpdateScreen();
            color++;
        }
    }
 
    if (selectedPolySegs.Count>0)
    {
        //do whatever work required for each of the selected segments
    }
}

See the video clip below:






Sunday, September 25, 2022

Update Entities via Data Binding of Modeless WPF Window - Update

I arrived at New Orleans for the AU2002 early and found myself having a bit extra time do deal with something that bothers me: in the article I published a couple of days ago on the topic of binding entity property with WPF modeless UI, I only demonstrated how to control entity's property (Rotation property of a block reference) from UI side. As a better AutoCAD plugin UI design, I should/could have made the binding between the entities and UI work in both ways: the user can control the entity property from the UI, and the UI can also reflect changes of the entity property made by the user in the AutoCAD editor. So, I quickly reworked the previous project.

Here were two things I took into account that happen in AutoCAD editor and ought to be reflected in the modeless UI:

1. The Rotation property of a monitored block reference can be changed by the CAD user in AutoCAD editor (through "Rotate" command, or simply via the block reference's grip). When this happens, the UI should reflect the change;

2. A monitored block reference (e.g. it is selected by the user to show up in the modeless UI) can be erased from AutoCAD editor, either by the user manually, or by other custom command. When this happens, the UI should get rid of the block reference from its monitored block list.

While the code changes were quite easy because the fully workable code in the previous project, I decided to create a brand new DLL project, and copy the code over for refactoring, in which I renamed most classes.

Although quite portions of the code here are the same as the previous project, I post all the code of this updated project here for the benefit of my readers' convenience. Here it goes.

The WPF support code:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
 
namespace BlockRotationDashboard
{
    public class RelayCommand : ICommand
    {
        private Action<object> execute;
        private Func<objectbool> canExecute;
 
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
 
        public RelayCommand(Action<objectexecute, Func<objectboolcanExecute = null)
        {
            this.execute = execute;
            this.canExecute = canExecute;
        }
 
        public bool CanExecute(object parameter)
        {
            return this.canExecute == null || this.canExecute(parameter);
        }
 
        public void Execute(object parameter)
        {
            this.execute(parameter);
        }
    }
 
    public abstract class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
 
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(thisnew PropertyChangedEventArgs(propertyName));
        }
    }
}

The CAD operations:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace BlockRotationDashboard
{
    public class CadHelper
    {
        public static ObjectId SelectBlock()
        {
            var dwg = Application.DocumentManager.MdiActiveDocument;
            var opt = new PromptEntityOptions(
                "\nSelect a block:");
            opt.SetRejectMessage("\nInvalid selection: not a block!");
            opt.AddAllowedClass(typeof(BlockReference), true);
            var res = dwg.Editor.GetEntity(opt);
            if (res.Status == PromptStatus.OK)
            {
                return res.ObjectId;
            }
            else
            {
                return ObjectId.Null;
            }
        }
 
        public static void WriteMessage(string msg)
        {
            var dwg = Application.DocumentManager.MdiActiveDocument;
            dwg.Editor.WriteMessage($"{msg}");
        }
 
        public static void ZoomToEntity(ObjectId entId)
        {
            Extents3d ext;
            using (var tran =
                entId.Database.TransactionManager.StartOpenCloseTransaction())
            {
                var ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
                ext = ent.GeometricExtents;
                tran.Commit();
            }
 
            ZoomToExtents(ext, 1.0);
        }
 
        public static void ZoomToExtents(Extents3d extdouble marginRatio)
        {
            var w = ext.MaxPoint.X - ext.MinPoint.X;
            var h = ext.MaxPoint.Y - ext.MinPoint.Y;
            var delta = Math.Max(w, h) * marginRatio;
 
            var minPt = new[]
            {
                ext.MinPoint.X-delta, ext.MinPoint.Y-delta, ext.MinPoint.Z
            };
            var maxPt = new[]
            {
                ext.MaxPoint.X+delta, ext.MaxPoint.Y+delta, ext.MaxPoint.Z
            };
 
            dynamic comApp =
                Autodesk.AutoCAD.ApplicationServices.Application.AcadApplication;
            comApp.ZoomWindow(minPt, maxPt);
        }
 
        public static void RotateBlock(ObjectId blkIdint rotation)
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            if (dwg.Database.FingerprintGuid != blkId.Database.FingerprintGuid)
            {
                throw new InvalidOperationException(
                    "Entity is not from current drawing database!");
            }
 
            using (dwg.LockDocument())
            {
                using (var tran =
                    blkId.Database.TransactionManager.StartTransaction())
                {
                    var blk =
                        (BlockReference)tran.GetObject(blkId, OpenMode.ForWrite);
                    blk.Rotation = Math.PI * rotation / 180.0;
                    tran.Commit();
                }
            }
            CadApp.UpdateScreen();
        }
    }
}

The "Model" in Model-View-ViewModel" pattern (I highlight the major code changes in red):

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace BlockRotationDashboard
{
    public class MonitoredBlock
    {
        public ObjectId ObjectId { getset; } = ObjectId.Null;
        public int Rotation { getset; } = 0;
    }
 
    public class MonitoredBlocks : Dictionary<string, List<MonitoredBlock>>
    {
        public MonitoredBlocks()
        {
            Application.DocumentManager.DocumentToBeDestroyed += (oe) =>
            {
                if (ContainsKey(e.Document.Database.FingerprintGuid))
                {
                    RemoveDatabaseEventHandlers(e.Document.Database);
                    Remove(e.Document.Database.FingerprintGuid);
                }
            };
        }
 
        public static Action<ObjectId, int> BlockAdded;
        public static Action<ObjectId> BlockRemoved;
        public static Action<ObjectId, int> BlockRotated;
        public static bool ChangedfromUI = false;
 
 
        public void AddBlock(ObjectId blkId)
        {
            var db = blkId.Database;
            int rotation = 0;
            using (var tran = db.TransactionManager.StartOpenCloseTransaction())
            {
                var blk = tran.GetObject(blkId, OpenMode.ForRead) as BlockReference;
                if (blk!=null) rotation= Convert.ToInt32(blk.Rotation * 180 / Math.PI);
            }
                
            var key = db.FingerprintGuid;
 
            if (!ContainsKey(key))
            {
                Add(key, new List<MonitoredBlock>());
                HookUpDatabaseEventHandlers(blkId.Database);
            }
 
            var mblk = new MonitoredBlock { ObjectId = blkId, Rotation = rotation };
            this[key].Add(mblk);
            BlockAdded?.Invoke(blkId, rotation);
        }
 
        public void RemoveBlock(ObjectId blkId)
        {
            var key = blkId.Database.FingerprintGuid;
            if (ContainsKey(key))
            {
                var blks = this[key];
                foreach (var blk in blks)
                {
                    if (blk.ObjectId == blkId)
                    {
                        blks.Remove(blk);
                        return;
                    }
                }
            }
        }
 
        public bool IsDoubleSelected(ObjectId blkId)
        {
            var key = blkId.Database.FingerprintGuid;
            if (ContainsKey(key))
            {
                foreach (var blk in this[key])
                {
                    if (blk.ObjectId == blkId) return true;
                }
                return false;
            }
            else
            {
                return false;
            }
        }
 
        private void HookUpDatabaseEventHandlers(Database db)
        {
            db.ObjectModified += Db_ObjectModified;
            db.ObjectErased += Db_ObjectErased;
        }
 
        private void Db_ObjectErased(object sender, ObjectErasedEventArgs e)
        {
            var entId = e.DBObject.ObjectId;
            if (ContainsKey(entId.Database.FingerprintGuid))
            {
                var blks = this[entId.Database.FingerprintGuid];
                foreach (var blk in blks)
                {
                    if (blk.ObjectId == entId)
                    {
                        blks.Remove(blk);
                        BlockRemoved?.Invoke(entId);
                        break;
                    }
                }
            }
        }
 
        private void Db_ObjectModified(object sender, ObjectEventArgs e)
        {
            if (ChangedfromUI) return;
 
            var entId = e.DBObject.ObjectId;
            if (ContainsKey(entId.Database.FingerprintGuid))
            {
                var blks = this[entId.Database.FingerprintGuid];
                foreach (var blk in blks)
                {
                    if (blk.ObjectId==entId)
                    {
                        var rotate = Convert.ToInt32(((BlockReference)e.DBObject).Rotation * 180 / Math.PI);
                        if (rotate != blk.Rotation)
                        {
                            blk.Rotation= rotate;
                            BlockRotated?.Invoke(blk.ObjectId, rotate);
                        }
                        break;
                    }
                }
            }
        }
 
        private void RemoveDatabaseEventHandlers(Database db)
        {
            try
            {
                db.ObjectErased -= Db_ObjectErased;
                db.ObjectModified -= Db_ObjectModified;
            }
            catch { }
        }
    }
}

The "View" in "Model-View-ViewModel" pattern (no major changes made to the 2 views at all, including the very few lines of code-behind remaining unchanged, except for changing the Slider's interval from 30 to 1, so that the UI can control/reflect block's rotation as small as 1 degree):

The UserControl class "BlockMonitorRowView":

<UserControl x:Class="BlockRotationDashboard.BlockMonitorRowView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:BlockRotationDashboard"
             mc:Ignorable="d" 
             d:DesignHeight="36" d:DesignWidth="650">
    <Grid Height="36">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="100*"/>
            <ColumnDefinition Width="70"/>
            <ColumnDefinition Width="70"/>
        </Grid.ColumnDefinitions>
        <StackPanel Grid.Column="0" Orientation="Horizontal">
            <TextBlock Text="Handle:" VerticalAlignment="Center" />
            <TextBlock Text="{Binding Handle, Mode=OneTime}" VerticalAlignment="Center" Margin="10,0,0,0"/>
        </StackPanel>
        <StackPanel Grid.Column="1" Orientation="Horizontal">
            <TextBlock Text="Rotation:" VerticalAlignment="Center" />
            <TextBlock Text="{Binding Rotation, Mode=OneWay}" VerticalAlignment="Center" Margin="10,0,0,0"/>
        </StackPanel>
        <Slider Grid.Column="2" VerticalAlignment="Center" Minimum="0" Maximum="360" 
                Interval="1" IsSnapToTickEnabled="True" Value="{Binding Rotation}" 
                TickPlacement="Both" LargeChange="1" SmallChange="1" TickFrequency="1"/>
        <Button Grid.Column="3" Content="Zoom" Width="60" Height="22" 
                VerticalAlignment="Center" HorizontalAlignment="Right"
                Command="{Binding ZoomCommand}" CommandParameter="{Binding BlockId}"/>
        <Button Grid.Column="4" Content="Remove" Width="60" Height="22" 
                VerticalAlignment="Center" HorizontalAlignment="Right"
                Command="{Binding RemoveCommand}" CommandParameter="{Binding BlockId}"/>
    </Grid>
</UserControl>

The Window class "BlockMonitorView":

<Window x:Class="BlockRotationDashboard.BlockMonitorView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:BlockRotationDashboard"
             mc:Ignorable="d" d:DesignWidth="800"
        Closing="TheWindow_Closing"
        x:Name="TheWindow"
        Width="660" MinHeight="250"
        SizeToContent="Height" ShowInTaskbar="False" ResizeMode="NoResize"
        Title="Block Rotation Dashboard" Height="225">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition Height="150"/>
        </Grid.RowDefinitions>
        <Grid Grid.Row="0" Margin="5,0,5,0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="200*"/>
                <ColumnDefinition Width="100"/>
            </Grid.ColumnDefinitions>
            <StackPanel Grid.Column="0" Orientation="Horizontal">
                <TextBlock Text="Monitored Blocks:" VerticalAlignment="Center"/>
                <TextBlock Text="{Binding BlockCount}" VerticalAlignment="Center" 
                           FontWeight="DemiBold" Foreground="Blue" Margin="10,0,0,0"/>
            </StackPanel>
            <Button Grid.Column="1" Content="Add Block >" Width="80" Height="22" 
                    VerticalAlignment="Center" HorizontalAlignment="Right"
                    Command="{Binding AddBlockCommand}" 
                    CommandParameter="{Binding ElementName=TheWindow}"/>
        </Grid>
        <ListBox Grid.Row="1" Margin="5,5,5,5"
                 ItemsSource="{Binding Blocks}" HorizontalContentAlignment="Stretch">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <local:BlockMonitorRowView DataContext="{Binding}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

This is the window's code-behind:

using System.Windows;
 
namespace BlockRotationDashboard
{
    /// <summary>
    /// Interaction logic for BlockMonitorView.xaml
    /// </summary>
    public partial class BlockMonitorView : Window
    {
        public BlockMonitorView()
        {
            InitializeComponent();
        }
 
        public BlockMonitorView(BlockMonitorViewModel viewModel) : this()
        {
            Loaded += (oe) =>
            {
                DataContext = viewModel;
            };
        }
 
        private void TheWindow_Closing(
            object sender, System.ComponentModel.CancelEventArgs e)
        {
            e.Cancel = true;
            Visibility = Visibility.Hidden;
        }
    }
}

The "ViewModel" of the "Model-View-ViewModel" pattern:

Firstly, the "BlockMonitorRowViewModel" class, which is the same as the one used in previous project, except for being renamed:

using Autodesk.AutoCAD.DatabaseServices;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace BlockRotationDashboard
{
    public class BlockMonitorRowViewModel : ViewModelBase
    {
        private int _rotation = 0;
        private ObjectId _blkId = ObjectId.Null;
 
        public BlockMonitorRowViewModel(ObjectId blkIdint rotation)
        {
            _blkId = blkId;
            _rotation = rotation;
            RemoveCommand = new RelayCommand(
                RemoveBlock, (o) => { return true; });
            ZoomCommand = new RelayCommand(
                ZoomToBlock, (o) => { return true; });
 
        }
        public string Handle => _blkId.Handle.ToString();
        public ObjectId BlockId => _blkId;
        public int Rotation
        {
            get => _rotation;
            set
            {
                _rotation = value;
                OnPropertyChanged("Rotation");
                RotateBlockAction?.Invoke(_blkId, _rotation);
            }
        }
        public static Action<ObjectId> RemoveBlockAction;
        public static Action<ObjectId> ZoomAction;
        public static Action<ObjectId, int> RotateBlockAction;
        public RelayCommand RemoveCommand { private setget; }
        public RelayCommand ZoomCommand { private setget; }
 
        private void RemoveBlock(object cmdParam)
        {
            RemoveBlockAction?.Invoke(_blkId);
        }
 
        private void ZoomToBlock(object cmdParam)
        {
            ZoomAction?.Invoke(_blkId);
        }
    }
}

The "BlockMonitorViewModel" class, in which I added some comments at changed code lines:

using Autodesk.AutoCAD.DatabaseServices;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace BlockRotationDashboard
{
    public class BlockMonitorViewModel : ViewModelBase
    {
        private MonitoredBlocks _monitoredBlocks;
        private ObservableCollection<BlockMonitorRowViewModel> _blocks;
        private string _currentDbGuid = "";
 
        public BlockMonitorViewModel(MonitoredBlocks monitoredBlocks)
        {
            _monitoredBlocks = monitoredBlocks;
            _blocks = new ObservableCollection<BlockMonitorRowViewModel>();
            AddBlockCommand = new RelayCommand(
                AddBlockToDashboard, (o) => { return true; });
 
            MonitoredBlocks.BlockAdded = OnBlockAdded;
 
            // The actions to be triggered when monitored blocks
            // is rotated or erased in AutoCAD editor
            MonitoredBlocks.BlockRemoved = OnBlockRemoved;
            MonitoredBlocks.BlockRotated = OnBlockRotated;
 
            BlockMonitorRowViewModel.RemoveBlockAction = OnBlockRemove;
            BlockMonitorRowViewModel.ZoomAction = OnBlockZoom;
            BlockMonitorRowViewModel.RotateBlockAction = OnBlockRotate;
 
            _currentDbGuid =
                CadApp.DocumentManager.MdiActiveDocument.Database.FingerprintGuid;
            ResetView();
 
            CadApp.DocumentManager.DocumentActivated += (oe) =>
            {
                if (e.Document.Database.FingerprintGuid != _currentDbGuid)
                {
                    _currentDbGuid = e.Document.Database.FingerprintGuid;
                    ResetView();
                }
            };
        }
 
        public ObservableCollection<BlockMonitorRowViewModel> Blocks => _blocks;
        public int BlockCount => _blocks.Count;
        public RelayCommand AddBlockCommand { private setget; }
 
        #region private methods
 
        private void ResetView()
        {
            _blocks.Clear();
            if (_monitoredBlocks.ContainsKey(_currentDbGuid))
            {
                var blks = _monitoredBlocks[_currentDbGuid];
                foreach (var blk in blks)
                {
                    _blocks.Add(new BlockMonitorRowViewModel(blk.ObjectId, blk.Rotation));
                }
            }
            OnPropertyChanged("BlockCount");
        }
 
        private void AddBlockToDashboard(object cmdParam)
        {
            var window = cmdParam as System.Windows.Window;
            var blkId = ObjectId.Null;
            try
            {
                if (window != null) window.Visibility = System.Windows.Visibility.Hidden;
                CadApp.MainWindow.Focus();
                while (true)
                {
                    blkId = CadHelper.SelectBlock();
                    if (!blkId.IsNull)
                    {
                        if (_monitoredBlocks.IsDoubleSelected(blkId))
                        {
                            CadHelper.WriteMessage(
                                "\nThe block has already been listed in rotation dashboard!");
                        }
                        else
                        {
                            break;
                        }
                    }
                    else
                    {
                        break;
                    }
                }
            }
            finally
            {
                if (window != null) window.Visibility =
                        System.Windows.Visibility.Visible;
            }
 
            if (blkId == ObjectId.Null) return;
 
            _monitoredBlocks.AddBlock(blkId);
        }
 
        private void OnBlockAdded(ObjectId blkIdint rotation)
        {
            _blocks.Add(new BlockMonitorRowViewModel(blkId, rotation));
            OnPropertyChanged("BlockCount");
        }
 
        // This method gets called when block reference in drawing is erased
        private void OnBlockRemoved(ObjectId blkId)
        {
            foreach (var blk in _blocks)
            {
                if (blk.BlockId == blkId)
                {
                    _blocks.Remove(blk);
                    break;
                }
            }
            OnPropertyChanged("BlockCount");
        }
 
        // This method gets called when user clicks "remove" button in the UI
        // which will stop the block's rotation being monitored
        private void OnBlockRemove(ObjectId blkId)
        {
            _monitoredBlocks.RemoveBlock(blkId);
            foreach (var blk in _blocks)
            {
                if (blk.BlockId == blkId)
                {
                    _blocks.Remove(blk);
                    break;
                }
            }
            OnPropertyChanged("BlockCount");
        }
 
        private void OnBlockZoom(ObjectId blkId)
        {
            CadHelper.ZoomToEntity(blkId);
        }
 
        // this method gets called when the monitored block is rotated in
        // AutoCAD editor
        private void OnBlockRotated(ObjectId blkIdint rotation)
        {
            foreach (var row in _blocks)
            {
                if (row.BlockId==blkId)
                {
                    row.Rotation = rotation;
                    break;
                }
            }
        }
 
        // This method gets called when user changes Slider's value in the UI
        private void OnBlockRotate(ObjectId bkIdint rotation)
        {
            try
            {
                MonitoredBlocks.ChangedfromUI = true;
                CadHelper.RotateBlock(bkId, rotation);
            }
            finally
            {
                MonitoredBlocks.ChangedfromUI = false;
            }
        }
 
        #endregion
    }
}

Then finally the CommandClass/Method, which is the same as the previous project:

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assembly: CommandClass(typeof(BlockRotationDashboard.MyCommands))]
 
namespace BlockRotationDashboard
{
    public class MyCommands 
    {
        private static MonitoredBlocks _blocks = null;
        private static BlockMonitorView _view = null;
        private static BlockMonitorViewModel _viewModel = null;
 
        [CommandMethod("MonitorBlockRotation", CommandFlags.Session)]
        public static void MonitorBlocks()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            if (_blocks == null)
            {
                _blocks = new MonitoredBlocks();
            }
 
            if (_view == null)
            {
                _viewModel = new BlockMonitorViewModel(_blocks);
                _view = new BlockMonitorView(_viewModel);
                CadApp.ShowModelessWindow(_view);
            }
            else
            {
                _view.Visibility = System.Windows.Visibility.Visible;
            }
        }
    }
}

Thanks to the use of MVVM pattern, the changed "business requirement" of monitoring the blocks' change in AutoCAD editor and reflecting the changes in the modeless UI is rather easy: the UI code (XAML code and the bit of code-behind) remains unchanged at all; so is the code for AutoCAD operation in class "CadHelper". I only updated the "Model" (class MonitoredBlocks) to have it hook up with Database_ObjectModified/ObjectErase events, so that when monitored change happens, the "Model" notifies the "ViewModel" and then the "View" gets updated.

If a Win Form is used, it is likely the UI's code-behind has to be updated, unless certain design pattern is used, such as model-view-controller/presenter that are suitable to Win form. I used such pattern with Win Form before and they are no match to MVVM pattern for WPF.

See the video clip showing the updated action:


The source code can be downloaded here.

Bug Fixing Update - 2022-09-29

In the discussion thread which inspired me for this article and the previous article, one reader (thank you, Genésio Hanauer!) pointed out that Database.FingerpointGuid is not unique if the drawings opened in the AutoCAD session are originated from the same drawing (such as they are created from the same template drawing), thus using Database.FingerprintGuid as the key for the Dictionary to store data model in order to segregate data from drawings could result error. So, I updated the class MonitoredBlocks to use Database.UnmanagedObject, which is the type of IntPtr, as the Dictionary's key. Here is the updated code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using System;
using System.Collections.Generic;
 
namespace BlockRotationDashboard
{
    public class MonitoredBlock
    {
        public ObjectId ObjectId { getset; } = ObjectId.Null;
        public int Rotation { getset; } = 0;
    }
 
    public class MonitoredBlocks : Dictionary<IntPtr, List<MonitoredBlock>>
    {
        public MonitoredBlocks()
        {
            Application.DocumentManager.DocumentToBeDestroyed += (oe) =>
            {
                if (ContainsKey(e.Document.Database.UnmanagedObject))
                {
                    RemoveDatabaseEventHandlers(e.Document.Database);
                    Remove(e.Document.Database.UnmanagedObject);
                }
            };
        }
 
        public static Action<ObjectId, int> BlockAdded;
        public static Action<ObjectId> BlockRemoved;
        public static Action<ObjectId, int> BlockRotated;
        public static bool ChangedfromUI = false;
 
 
        public void AddBlock(ObjectId blkId)
        {
            var db = blkId.Database;
            int rotation = 0;
            using (var tran = db.TransactionManager.StartOpenCloseTransaction())
            {
                var blk = tran.GetObject(blkId, OpenMode.ForRead) as BlockReference;
                if (blk!=null) rotation= Convert.ToInt32(blk.Rotation * 180 / Math.PI);
            }
 
            var key = db.UnmanagedObject;
 
            if (!ContainsKey(key))
            {
                Add(key, new List<MonitoredBlock>());
                HookUpDatabaseEventHandlers(blkId.Database);
            }
 
            var mblk = new MonitoredBlock { ObjectId = blkId, Rotation = rotation };
            this[key].Add(mblk);
            BlockAdded?.Invoke(blkId, rotation);
        }
 
        public void RemoveBlock(ObjectId blkId)
        {
            var key = blkId.Database.UnmanagedObject;
            if (ContainsKey(key))
            {
                var blks = this[key];
                foreach (var blk in blks)
                {
                    if (blk.ObjectId == blkId)
                    {
                        blks.Remove(blk);
                        return;
                    }
                }
            }
        }
 
        public bool IsDoubleSelected(
            ObjectId blkId)
        {
            var key = blkId.Database.UnmanagedObject;
            if (ContainsKey(key))
            {
                foreach (var blk in this[key])
                {
                    if (blk.ObjectId == blkId) return true;
                }
                return false;
            }
            else
            {
                return false;
            }
        }
 
        private void HookUpDatabaseEventHandlers(Database db)
        {
            db.ObjectModified += Db_ObjectModified;
            db.ObjectErased += Db_ObjectErased;
        }
 
        private void Db_ObjectErased(object sender, ObjectErasedEventArgs e)
        {
            var entId = e.DBObject.ObjectId;
            if (ContainsKey(entId.Database.UnmanagedObject))
            {
                var blks = this[entId.Database.UnmanagedObject];
                foreach (var blk in blks)
                {
                    if (blk.ObjectId == entId)
                    {
                        blks.Remove(blk);
                        BlockRemoved?.Invoke(entId);
                        break;
                    }
                }
            }
        }
 
        private void Db_ObjectModified(object sender, ObjectEventArgs e)
        {
            if (ChangedfromUI) return;
 
            var entId = e.DBObject.ObjectId;
            if (ContainsKey(entId.Database.UnmanagedObject))
            {
                var blks = this[entId.Database.UnmanagedObject];
                foreach (var blk in blks)
                {
                    if (blk.ObjectId==entId)
                    {
                        var rotate = Convert.ToInt32(((BlockReference)e.DBObject).Rotation * 180 / Math.PI);
                        if (rotate != blk.Rotation)
                        {
                            blk.Rotation= rotate;
                            BlockRotated?.Invoke(blk.ObjectId, rotate);
                        }
                        break;
                    }
                }
            }
        }
 
        private void RemoveDatabaseEventHandlers(Database db)
        {
            try
            {
                db.ObjectErased -= Db_ObjectErased;
                db.ObjectModified -= Db_ObjectModified;
            }
            catch { }
        }
    }
}

I have also updated the source that can be downloaded in the link above.

Followers

About Me

My photo
After graduating from university, I worked as civil engineer for more than 10 years. It was AutoCAD use that led me to the path of computer programming. Although I now do more generic business software development, such as enterprise system, timesheet, billing, web services..., AutoCAD related programming is always interesting me and I still get AutoCAD programming tasks assigned to me from time to time. So, AutoCAD goes, I go.