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.

4 comments:

Kelcyo said...

The update was great...

Karsten Loewen said...

Norm,

Such a great post! I've learned a lot about databinding from it.

One question: Using the slider in WPF window is really unfriendly with undo/redo actions due to multiple consecutive transactions in the CadHelper.RotateBlock method. Would it possible to capture all rotation changes in one transaction? Or would one have implement a jig to do something like that.

Thanks!

Norman Yuan said...

Hi Karsten,

I do not see a case like this (a modeless UI with a user control continuously causing an entity changes) have much practical value, maybe only be useful in very limited scenarios. Because of the changes are triggered from modeless UI, where user can leave the UI intentionally or unintentionally, it would be problematic if we only use one transaction and expect user to decide when the commit it.

If being able to undo is important to your process, I'd use modal dialog (not very sure, though, that with modal dialog showing, AutoCAD editor can still update the screen or not with a transaction pending to be committed. TransactionManager.QueueForGraphcsFlush() might be required). Or, you can manage to set an undo mark prior to allow user interact with the modeless UI. I'd definitely go with modal UI first.

Regards,

Norman

Kelcyo said...

I detected that if the star window is still in visualization, and the rotation property is changed directly in the model, the change is not reflected in the window, here it needs this update.. even so, the code was great

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.