Monday, September 30, 2013

Raising And Subscribing Events From Custom PaletteSet

With AutoCAD .NET API, we can build custom PaletteSet as needed. In the custom PaletteSet multiple Palettes (UserControls of Windows Form or WPF) can be hosted and each Palette can host many Windows Form Controls or WPF Controls, depending on the needs.

Note: to simplify the discussion, this article limits its discussion on using Windows Form UserControl as Palette.

Usually, the user interaction with the Controls in a Palette is handled at the Palette/UserControl level when events associated to the Control raised due to user's action (clicking button, selecting item in a dropdown list, selecting a row in ComboBox/ListBox/ListView/DataGridView...). However, sometimes, it may be desired to expose the event at PaletteSet level, so some components outside the PaletteSet can catch the event and do something, according to what user has interacted with the Control in a Palette.

Take this situation for instance: I may want to have a custom PaletteSet showing. When user clicks a button on the first Palette, some code will run to processing current active drawing.

Well, it can be done easily by just writing code at the button's Button_Click() event handler. That is simple and straightforward. However, in this code fashion, the process logic is tightly coupled with UI module (UserControl/Form's code behind), which makes the code difficult to maintain.

The other approach is to bubble up the events raised by Controls to the PaletteSet level, and then it is up to other components that subscribe the events of the PaletteSet to decide how to handle the events based on the information passed in event arguments. This way, the UI part (the PaletteSet) would only focus on presenting data to the user and receiving user interaction and firing event with necessary event arguments, and does not need to know the details of real work done by the other components. Therefore the UI side code is effectively separated from the code that does the heavy lifting.

There are many different design patterns around, such as MVC, MVP, MVVM... that help programmers to build more maintainable application. What I show here is just a very basic thing to do toward to that direction to make a UI component (custom PaletteSet) only mind its own business: presenting data and pass user interaction to other components via events.

For example, I am about to write some code to process drawing information, say, firstly searching drawing for particular block or blocks, and then update the drawing based on the search result. No problem. I create a tool, a class, and have it to do all the required work against a drawing in AutoCAD. Based on the drafting process, this tool should take action according to user's interaction with AutoCAD, or interaction with some kind of custom UI added to AutoCAD. However, I do not want to tie this tool to a specific UI, because the functionality of this tool can be used in different drafting workflow. I'd like its drawing processing action to be activated external UI interaction without having to know what the UI is.

In order to achieve this goal, I first define a s Interface that in turn defines what user interaction  my custom AutoCAD tool expects. Here is the code:

    1 using System;
    2 using Autodesk.AutoCAD.Windows;
    3 
    4 namespace PeletteEvents
    5 {
    6     public interface ICustomPaletteSet
    7     {
    8         event PaletteDropdownSelectedIndexChangedEventhandler
    9             PaletteDropdownSelectedIndexChanged;
   10 
   11         event PaletteButtonClickEventHandler
   12             PaletteButtonClicked;
   13 
   14         //This event does not have to be implemented in
   15         //custom PaletteSet, because PaletteSet has already
   16         //have this event exposed
   17         event PaletteActivatedEventHandler
   18             PaletteActivated;
   19 
   20         //This property does not have be implemented in
   21         //custom PalatteSet, because PaletteSet has a property
   22         //"Visible" for read and write
   23         bool Visible { set; get; }
   24     }
   25 
   26     public abstract class PaletteEventArgs:EventArgs
   27     {
   28         private string _paletteName;
   29         public PaletteEventArgs(string paletteName)
   30         {
   31             _paletteName = paletteName;
   32         }
   33 
   34         public string PaletteName
   35         {
   36             get { return _paletteName; }
   37         }
   38     }
   39 
   40     public class PaletteButtonClickedEventArgs : PaletteEventArgs
   41     {
   42         private string _buttonName;
   43         public PaletteButtonClickedEventArgs(
   44             string buttonName, string paletteName)
   45             :base(paletteName)
   46         {
   47             _buttonName = buttonName;
   48         }
   49 
   50         public string ButtonName
   51         {
   52             get { return _buttonName; }
   53         }
   54     }
   55 
   56     public class PaletteDropdownSelectedIndexChangedEventArgs :
   57         PaletteEventArgs
   58     {
   59         private int _selectedIndex;
   60         private string _selectedText;
   61 
   62         public PaletteDropdownSelectedIndexChangedEventArgs(
   63             int selectedIndex, string selectedText, string paletteName)
   64             : base(paletteName)
   65         {
   66             _selectedIndex = selectedIndex;
   67             _selectedText = selectedText;
   68         }
   69 
   70         public int SelectedIndex
   71         {
   72             get { return _selectedIndex; }
   73         }
   74 
   75         public string SelectedText
   76         {
   77             get { return _selectedText; }
   78         }
   79     }
   80 
   81     public delegate void PaletteButtonClickEventHandler(
   82         object sender, PaletteButtonClickedEventArgs e);
   83 
   84     public delegate void PaletteDropdownSelectedIndexChangedEventhandler(
   85         object sender, PaletteDropdownSelectedIndexChangedEventArgs e);
   86 }

Here is my custom tool that processes AutoCAD drawing:

    1 using Autodesk.AutoCAD.ApplicationServices;
    2 using Autodesk.AutoCAD.EditorInput;
    3 using Autodesk.AutoCAD.Windows;
    4 
    5 namespace PeletteEvents
    6 {
    7     //A drawing process tool that is driven by the custom PaletteSet
    8     //That is, this class subscribe the events raised by MyPaletteSet
    9     public class DrawingProcessTool
   10     {
   11         private ICustomPaletteSet _ps;
   12         private DocumentCollection _docs;
   13         public DrawingProcessTool(ICustomPaletteSet pSet)
   14         {
   15             _docs = Application.DocumentManager;
   16 
   17             _ps = pSet;
   18             _ps.PaletteDropdownSelectedIndexChanged +=
   19                 new PaletteDropdownSelectedIndexChangedEventhandler(
   20                     _ps_PaletteDropdownSelectedIndexChanged);
   21             _ps.PaletteButtonClicked += new
   22                 PaletteButtonClickEventHandler(_ps_PaletteButtonClicked);
   23 
   24             _ps.PaletteActivated += new PaletteActivatedEventHandler(
   25                 _ps_PaletteActivated);
   26         }
   27 
   28         #region Event handlers
   29 
   30         private void _ps_PaletteActivated(object sender, PaletteActivatedEventArgs e)
   31         {
   32             Document dwg = _docs.MdiActiveDocument;
   33 
   34             //Do something with the drawing after user clicks
   35             //a button on one of the palette in the PaletteSet
   36             dwg.Editor.WriteMessage("\nPalette \"{0}\" activated, " +
   37                 "now some process with this dwg starts...", e.Activated.Name);
   38 
   39             //Do something with current active drawing
   40 
   41             dwg.Editor.WriteMessage(
   42                 "\nProcessing triggered by palette button-click has completed!");
   43             Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
   44         }
   45 
   46         private void _ps_PaletteButtonClicked(object sender,
   47             PaletteButtonClickedEventArgs e)
   48         {
   49             Document dwg = _docs.MdiActiveDocument;
   50 
   51             //Do something with the drawing after user clicks
   52             //a button on one of the palette in the PaletteSet
   53             dwg.Editor.WriteMessage(
   54                 "\nButtom \"{0}\" on Palette \"{1}\" clicked, " +
   55                 "now some process with this dwg starts...",
   56                 e.ButtonName, e.PaletteName);
   57 
   58             //Do something with current active drawing
   59             UpdateDrawingDatabase(dwg);
   60 
   61             dwg.Editor.WriteMessage(
   62                 "\nProcessing triggered by palette button-click has completed!");
   63             Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
   64         }
   65 
   66         private void _ps_PaletteDropdownSelectedIndexChanged(object sender,
   67             PaletteDropdownSelectedIndexChangedEventArgs e)
   68         {
   69             Document dwg = _docs.MdiActiveDocument;
   70 
   71             //Do something with the drawing after user selects 
   72             //a item in the dropdown list of the palette in the PaletteSet
   73             dwg.Editor.WriteMessage(
   74                 "\nItem \"{0}\" in dropdown list on Palette \"{1}\" is selected, " +
   75                 "now some process with this dwg starts...",
   76                 e.SelectedText, e.PaletteName);
   77 
   78             //Do something with current active drawing
   79             SearchBlocks(e.SelectedText, dwg);
   80 
   81             dwg.Editor.WriteMessage(
   82                 "\nProcessing triggered by dropdown list selecting has completed!");
   83             Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
   84         }
   85 
   86         #endregion
   87 
   88         #region private methods that do the real drawing processing work
   89 
   90         private void SearchBlocks(string blockName, Document dwg)
   91         {
   92             //Do searching work
   93             Editor ed = dwg.Editor;
   94 
   95             ed.WriteMessage("\n===============================================");
   96             ed.WriteMessage("\nSearching block...Please wait...");
   97             ed.WriteMessage("\nSearching completed");
   98             ed.WriteMessage("\n===============================================");
   99         }
  100 
  101         private void UpdateDrawingDatabase(Document dwg)
  102         {
  103             //Do work here
  104             Editor ed = dwg.Editor;
  105 
  106             ed.WriteMessage("\n===============================================");
  107             ed.WriteMessage("\nUpdating drawing database...Please wait...");
  108             ed.WriteMessage("\nUpdating completed");
  109             ed.WriteMessage("\n===============================================");
  110         }
  111 
  112         #endregion
  113     }
  114 }
 
To be focused on the topic of this post, I deliberately make this class not expose drawing process functions as public methods. From its Constructor, as the code shows, it is passed in the interface ICustomPaletteSet, an abstract, which can be however the UI designed/developer wants, as long as meets the ICustomPaletteSet's requirement. The tool subscribe the events of the ICustomPaletteSet, and act according to the event its receives from the UI.
 
Now, I am free to build UI that is used to drive my custom tool's work. As long as my UI implements the ICustomPalatteSet, the UI does not need to know what the tool it drives is going to do.
 
In this simple example, I created 2 Windows Form UserControls as Palette: one with a combo box, and other with a button. See pictures below.
 
 
 
The code for the 2 UserControls is following:

    1 using System;
    2 using System.Windows.Forms;
    3 
    4 namespace PeletteEvents
    5 {
    6     public partial class MyPaletteA : UserControl
    7     {
    8         private const string PALETTE_NAME = "My Palette A";
    9 
   10         public MyPaletteA()
   11         {
   12             InitializeComponent();
   13         }
   14 
   15         public string PaletteName
   16         {
   17             get { return PALETTE_NAME; }
   18         }
   19 
   20         public event PaletteDropdownSelectedIndexChangedEventhandler
   21             PaletteDropdownSelectedIndexChanged;
   22 
   23         private void cboList_SelectedIndexChanged(object sender, EventArgs e)
   24         {
   25             if (PaletteDropdownSelectedIndexChanged != null)
   26             {
   27                 string text;
   28                 int index;
   29                 ComboBox cbo=sender as ComboBox;
   30                 if (cbo.SelectedIndex<0)
   31                 {
   32                     index=-1;
   33                     text="";
   34                 }
   35                 else
   36                 {
   37                     index=cbo.SelectedIndex;
   38                     text=cbo.Text;
   39                 }
   40 
   41                 PaletteDropdownSelectedIndexChangedEventArgs args=
   42                     new PaletteDropdownSelectedIndexChangedEventArgs(
   43                         index, text, PALETTE_NAME);
   44                 PaletteDropdownSelectedIndexChanged(this, args);
   45             }
   46         }
   47     }
   48 }

    1 using System;
    2 using System.Windows.Forms;
    3 
    4 namespace PeletteEvents
    5 {
    6     public partial class MyPaletteB : UserControl
    7     {
    8         private const string PALETTE_NAME = "My Palette B";
    9 
   10         public MyPaletteB()
   11         {
   12             InitializeComponent();
   13         }
   14 
   15         public string PaletteName
   16         {
   17             get { return PALETTE_NAME; }
   18         }
   19 
   20         public event PaletteButtonClickEventHandler PaletteButtonClicked;
   21 
   22         private void btnClickMe_Click(object sender, EventArgs e)
   23         {
   24             if (PaletteButtonClicked != null)
   25             {
   26                 Button btn = sender as Button;
   27                 string btnName = btn.Name;
   28 
   29                 PaletteButtonClickedEventArgs args =
   30                     new PaletteButtonClickedEventArgs(btnName, PALETTE_NAME);
   31 
   32                 PaletteButtonClicked(this, args);
   33             }
   34         }
   35     }
   36 }

Now here is the whole UI stuff, a custom PaletteSet, that implement the interface ICustonPaletteSet:

    1 using System;
    2 using Autodesk.AutoCAD.Windows;
    3 using System.Windows.Forms;
    4 
    5 namespace PeletteEvents
    6 {
    7     public class MyPaletteSet : PaletteSet, ICustomPaletteSet
    8     {
    9         public MyPaletteSet() :
   10             base("", new Guid("3935CBE1-B713-4691-8FB2-D69957DCBBBE"))
   11         {
   12             this.Dock = DockSides.None;
   13             this.MinimumSize = new System.Drawing.Size(300, 300);
   14             this.Size = new System.Drawing.Size(400, 400);
   15             this.Style = PaletteSetStyles.ShowAutoHideButton |
   16                 PaletteSetStyles.ShowCloseButton |
   17                 PaletteSetStyles.ShowTabForSingle |
   18                 PaletteSetStyles.Snappable;
   19             this.DockEnabled = DockSides.None;
   20 
   21             MyPaletteA paletteA = new MyPaletteA();
   22             paletteA.PaletteDropdownSelectedIndexChanged +=
   23                 new PaletteDropdownSelectedIndexChangedEventhandler(
   24                     paletteA_PaletteDropdownSelectedIndexChanged);
   25 
   26             this.Add(paletteA.PaletteName, paletteA);
   27 
   28             MyPaletteB paletteB = new MyPaletteB();
   29             paletteB.PaletteButtonClicked +=
   30                 new PaletteButtonClickEventHandler(
   31                     paletteB_PaletteButtonClicked);
   32 
   33             this.Add(paletteB.PaletteName, paletteB);
   34         }
   35 
   36         public event PaletteDropdownSelectedIndexChangedEventhandler
   37             PaletteDropdownSelectedIndexChanged;
   38 
   39         public event PaletteButtonClickEventHandler
   40             PaletteButtonClicked;
   41 
   42         private void paletteB_PaletteButtonClicked(object sender,
   43             PaletteButtonClickedEventArgs e)
   44         {
   45             MessageBox.Show("Button \"" + e.ButtonName + "\" has been clicked!");
   46 
   47             //Buble up the event
   48             if (PaletteButtonClicked != null)
   49             {
   50                 PaletteButtonClicked(this, e);
   51             }
   52         }
   53 
   54         private void paletteA_PaletteDropdownSelectedIndexChanged(object sender,
   55             PaletteDropdownSelectedIndexChangedEventArgs e)
   56         {
   57             MessageBox.Show("Dropdown list selected index has been changed:\n\n" +
   58                 "Current selected index=" + e.SelectedIndex + "\n" +
   59                 "Selected text=" + e.SelectedText);
   60 
   61             //Buble up the event
   62             if (PaletteDropdownSelectedIndexChanged != null)
   63             {
   64                 PaletteDropdownSelectedIndexChanged(this, e);
   65             }
   66         }
   67     }
   68 }

Put everything together, here is the command method to bring up the UI:

    1 using Autodesk.AutoCAD.Runtime;
    2 
    3 [assembly: CommandClass(typeof(PeletteEvents.MyCommands))]
    4 
    5 namespace PeletteEvents
    6 {
    7     public class MyCommands
    8     {
    9         private static ICustomPaletteSet _paletteSet = null;
   10         private static DrawingProcessTool _tool = null;
   11 
   12         [CommandMethod("PSEvents", CommandFlags.Session)]
   13         public static void ShowMyPS()
   14         {
   15             if (_paletteSet == null)
   16             {
   17                 _paletteSet = new MyPaletteSet();
   18             }
   19 
   20             if (_tool == null)
   21             {  
   22                 //Hook up the drawing processing component with
   23                 //UI - ICustomPaletteSet
   24                 _tool = new DrawingProcessTool(_paletteSet);
   25             }
   26 
   27             _paletteSet.Visible = true;
   28 
   29             Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();  
   30         }
   31     }
   32 }

From the code shown above we can see
  • The UI is only receive user interaction to its UI components (combo box and button, in this case) and relay the user interaction as events to the event subscriber. It does not know and does not care what the event subscriber does after receiving the events.
  • The event subscriber (class DrawingProcessTool) acts accordingly based in the event its receives and the data passed to is as EventArgs. It does not know or care what the UI is.
Here is a video clip showing the code in action.

As the code shown here, it seems fairly easy to use an Interface to effectively separate UI and real AutoCAD process logic. However, in the real work projects, things could be more complicated. To structure a good business solution needs careful plan before one dive into coding. I have to admit after many years of doing AutoCAD custom programming, I still often build AutoCAD custom tools with processing logic being mixed with UI and am a bit lazy to refactor the solution into a better structure once the solution can do what is expected to do.

No comments:

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.