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<object, bool> canExecute;
 
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
 
        public RelayCommand(Action<object> execute, Func<object, bool> canExecute = 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(this, new 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 ext, double 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 blkId, int 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 { get; set; } = ObjectId.Null;
        public int Rotation { get; set; } = 0;
    }
 
    public class MonitoredBlocks : Dictionary<string, List<MonitoredBlock>>
    {
        public MonitoredBlocks()
        {
            Application.DocumentManager.DocumentToBeDestroyed += (o, e) =>
            {
                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 += (o, e) =>
            {
                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 blkId, int 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 set; get; }
        public RelayCommand ZoomCommand { private set; get; }
 
        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 += (o, e) =>
            {
                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 set; get; }
 
        #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 blkId, int 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 blkId, int 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 bkId, int 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 { get; set; } = ObjectId.Null;
        public int Rotation { get; set; } = 0;
    }
 
    public class MonitoredBlocks : Dictionary<IntPtr, List<MonitoredBlock>>
    {
        public MonitoredBlocks()
        {
            Application.DocumentManager.DocumentToBeDestroyed += (o, e) =>
            {
                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.

Friday, September 23, 2022

Update Entities via Data Binding of Modeless WPF Window

This article is in response to a question asked in AutoCAD .NET API discussion forum. 

In last a few year, I use WPF as the main UI option in my AutoCAD .NET API development, only occasionally use Win Form for very simple UI for collecting user inputs. While one can use WPF in about the same way as with Win Form (e.g. writing a lot of code behind the UI to control the behaviors of the UI), the real advantages of using WPF are both its rich UI presentation capability and its data binding. That latter allows us programmer to easily separate the UI and business options, usually through a well-known design pattern MVVM (Model-View-ViewModel). 

The AutoCAD .NET project in this article does almost exactly the work as the question asked in the discussion forum:

1. Create a WPF modeless form;

2. On the form there are some WPF UserControls, which are bound to certain AutoCAD entities in drawing; In this project, the UserControls are WPF Slider controls, the entities are block refeeneces in drawing;

3. CAD user can change the slider's tick, hence the slider's value changes with a given range (0 to 360, in this case). The slider's Value property is bound to a block reference's Rotation property. Therefore, the the slider's value changes, the corresponding block reference in drawing rotates accordingly.

Let's dive into the code.

First some very basic code almost all WPF project needs: the implementation of ICommand and INotifyPropertyChanged:

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
 
namespace WPFBindingEntities
{
    public class RelayCommand : ICommand
    {
        private Action<object> execute;
        private Func<object, bool> canExecute;
 
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
 
        public RelayCommand(Action<object> execute, Func<object, bool> canExecute = 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(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Now the code for "Model" part of the Model-View-ViewModel pattern, class ControlledBlocks:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using System;
using System.Collections.Generic;
 
namespace WPFBindingEntities
{
    public class ControlledBlocks : Dictionary<string, List<ObjectId>>
    {
        public ControlledBlocks()
        {
            Application.DocumentManager.DocumentToBeDestroyed += (o, e) =>
            {
                if (ContainsKey(e.Document.Database.FingerprintGuid))
                {
                    Remove(e.Document.Database.FingerprintGuid);
                }
            };
        }
 
        public static Action<ObjectId, double> BlockAdded;
 
        public void AddBlock(ObjectId blkId)
        {
            var key = blkId.Database.FingerprintGuid;
 
            if (!ContainsKey(key))
            {
                Add(key, new List<ObjectId>());
            }
 
            this[key].Add(blkId);
            var rotation = 0.0;
            using (var tran=
                blkId.Database.TransactionManager.StartOpenCloseTransaction())
            {
                var blk=(BlockReference)tran.GetObject(blkId, OpenMode.ForRead);
                rotation= blk.Rotation;
                tran.Commit();
            }
 
            BlockAdded?.Invoke(blkId, rotation);
        }
 
        public void RemoveBlock(ObjectId blkId)
        {
            var key = blkId.Database.FingerprintGuid;
            if (ContainsKey(key))
            {
                var blkIds = this[key];
                foreach (var id in blkIds)
                {
                    if (id==blkId)
                    {
                        blkIds.Remove(id);
                        return;
                    }
                }
            }
        }
 
        public bool IsDoubleSelected(ObjectId blkId)
        {
            var key = blkId.Database.FingerprintGuid;
            if (ContainsKey(key))
            {
                return this[key].Contains(blkId);
            }
            else
            {
                return false;
            }
        }
    }
}

Because the WPF UI will be modeless, the data (selected block references, which rotation will be controlled by the modeless UI) should be organized in per-document/database base. I use a List<ObjectId> to hold selected block reference IDs and use each document's Database.FingerprintGuid property as the Dictionary's key. I could have used Document, or Document.UnmanagedObject as the key, but considering ObjectId has a property Database, using Database.FingerprintGuid is more convenient, which would become quite obvious if one looks into the code of this class. An static instance of this class is kept alive in entire AutoCAD session. So, inside the class, and event handle is hooked up to DocumentCollection.DocumentToBeDestroyed, so that when an open drawing is about to be closed down, the data corresponding to this document/database is cleared. One would also notice a public Action BlockAdded is exposed as a static property. If  it is assigned somewhere (in the ViewModel, in this case), whenever a block reference is added into this data model, this action is invoked (i.e. a method in ViewModel, which would update the UI to show the newly added block reference). This is a simplified version of the usual event handler delegate/event argument/event approach.

Now the "View" part of the Model-View-ViewModel. The UI, as a modeless window, looks like:

The main view is BlockRotationView class, its XAML code is as following:

<Window x:Class="WPFBindingEntities.BlockRotationView"
             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:WPFBindingEntities"
             mc:Ignorable="d"
        Closing="TheWindow_Closing"
        x:Name="TheWindow"
        Width="580" SizeToContent="Height" ShowInTaskbar="False" ResizeMode="NoResize"
        Title="Block Rotation Dashboard">
    <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:BlockRotationRowView DataContext="{Binding}" />
                </DataTemplate>
            </ListBox.ItemTemplate>           
        </ListBox>
    </Grid>
</Window>

As the window's XMAL code shows, the items in the list box is custom WPF UserControls of class BlockRotationRowView, its XAML code is as following:

<UserControl x:Class="WPFBindingEntities.BlockRotationRowView"
             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:WPFBindingEntities"
             mc:Ignorable="d" 
             d:DesignHeight="36" d:DesignWidth="500">
    <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.0" Maximum="360.0" 
                Interval="30" IsSnapToTickEnabled="True" Value="{Binding Rotation}" 
                TickPlacement="Both" LargeChange="30" SmallChange="30" TickFrequency="30"/>
        <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 XAML code is almost all I need here for the "view", except for only a few lines of C# code-behind:

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

The overloaded view constructor is to make the view data-bound to the view model, while the handling of TheWindow_Closing event is necessary because of the window being modeless: the Closing event handle makes the window effectively behaves as singleton instance.

Now there is "ViewModel" class corresponding to the "View" in the "Model-View-ViewModel": class BlockRotationViewModel and BlockRotationRowViewModel.

This the main view model BlockRotationViewModel class:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using System.Collections.ObjectModel;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace WPFBindingEntities
{
    public class BlockRotationViewModel : ViewModelBase
    {
        private ControlledBlocks _monitoredBlocks;
        private ObservableCollection<BlockRotationRowViewModel> _blocks;
        private string _currentDbGuid = "";
 
        public BlockRotationViewModel(ControlledBlocks monitoredBlocks)
        {
            _monitoredBlocks = monitoredBlocks;
            _blocks = new ObservableCollection<BlockRotationRowViewModel>();
            AddBlockCommand = new RelayCommand(
                AddBlockToDashboard, (o) => { return true; });
 
            ControlledBlocks.BlockAdded = OnBlockAdded;
            BlockRotationRowViewModel.RemoveBlockAction = OnBlockRemove;
            BlockRotationRowViewModel.ZoomAction = OnBlockZoom;
            BlockRotationRowViewModel.RotateBlockAction = OnBlockRotate;
 
            _currentDbGuid = 
                CadApp.DocumentManager.MdiActiveDocument.Database.FingerprintGuid;
            ResetView();
 
            CadApp.DocumentManager.DocumentActivated += (o, e) =>
            {
                if (e.Document.Database.FingerprintGuid != _currentDbGuid)
                {
                    _currentDbGuid = e.Document.Database.FingerprintGuid;
                    ResetView();
                }
            };
        }
 
        public ObservableCollection<BlockRotationRowViewModel> Blocks => _blocks;
        public int BlockCount => _blocks.Count;
        public RelayCommand AddBlockCommand { private set; get; }
 
        #region private methods
 
        private void ResetView()
        {
            _blocks.Clear();
            if (_monitoredBlocks.ContainsKey(_currentDbGuid))
            {
                var blkIds = _monitoredBlocks[_currentDbGuid];
                foreach (var Id in blkIds)
                {
                    _blocks.Add(new BlockRotationRowViewModel(Id, 0.0));
                }
            }
        }
 
        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;
                Application.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 blkId, double rotation)
        {
            _blocks.Add(new BlockRotationRowViewModel(blkId, rotation));
            OnPropertyChanged("BlockCount");
        }
 
        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);
        }
 
        private void OnBlockRotate(ObjectId bkId, double rotation)
        {
            CadHelper.RotateBlock(bkId, rotation);
        }
 
        #endregion
    }
}

And this is BlockRotationRowViewModel class:

using Autodesk.AutoCAD.DatabaseServices;
using System;
 
namespace WPFBindingEntities
{
    public class BlockRotationRowViewModel : ViewModelBase
    {
        private double _rotation = 0.0;
        private ObjectId _blkId = ObjectId.Null;
        
        public BlockRotationRowViewModel(ObjectId blkId, double 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 double 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, double> RotateBlockAction;
        public RelayCommand RemoveCommand { private set; get; }
        public RelayCommand ZoomCommand { private set; get; }   
 
        private void RemoveBlock(object cmdParam)
        {
            RemoveBlockAction?.Invoke(_blkId);
        }
 
        private void ZoomToBlock(object cmdParam)
        {
            ZoomAction?.Invoke(_blkId);
        }
    }
}

Of course, all the AutoCAD related operations are organized in a separate class CadHelper, exposed as static methods:


using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using System;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace WPFBindingEntities
{
    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 ext, double 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 blkId, double 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();
        }
    }
}

Finally, this the CommandClass, which keeps the data model (ControlledBlocks class), view (BlockRotationView class) and viewmodel (BlockRotationViewModel class) in the Application context as static instance (singleton) because of the view is required to be modeless window:

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

As one can see, doing this project with MVVM pattern, the entire operation is fairly well separated into Model, View, and ViewModel, taking advantage of WPF data binding.

As usual, see the video clip below showing the code in action:


While all the code in the project are posted inside this article, the entire source code can be downloaded here. It is VisualStudio2022 solution and tested against AutoCAD 2022, but it should work with all later versions of AutoCAD.




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.