Tuesday, September 28, 2010

Pluggable PaletteSet

In ObjectARX .NET API, AutoCAD.Windows.PaletteSet class makes creating dockable floating winidow in AutoCAD a pretty easy thing to do. Many AutoCAD programmers use PaletteSet as a UI container to host a series of Palettes (Windows Form User Controls, usually).

Prior to AutoCAD 2009, PaletteSet is sealed class, e.g. it cannot be inherited as a base class to derive your own custom PaletteSet. Since AutoCAD 2009, PaletteSet class is not sealed any more. This opens a possiblity for our AutoCAD programmer to create a custom, generic PaletteSet UI container once, and develop pluggable palette based on business need and plug into the PaletteSet when needed (without any code change to the PaletteSet).

In this article, I'll show how to use interface to define a pluggable PaletteSet. Interface is commonly used to define a set operations/properties for different classes to implement. It is one of the OO programming basic concept and used in .NET programming very often. However, there are many AutoCAD programmers who may not be a professional software developers and may too focused on AutoCAD API itself and did not explore some "advanced" programming technique enough, such as using "Interface" to simplify development tasks.

For this article, I demonstrate how to use Interface to create a pluggable PaletteSet. In Visual Stadio 2008, I started a class library project called MyPaletteSet, which includes 3 code files.

First code file is a interface that defines Palette that will be hosted in the pluggable PaletteSet: IThePalette. Later, when creating a Win Form UserControl as Palette, the UserControl will implement the interface. This, no matter how do you design your UserControls and how different they would be, to the hosting PaletteSet, they all are IThePalette. That is, the hosting PaletteSet does not have knowledge of each individual UserControl it hosts, as long as the UserControl is an IThePalette.

Here is the code:

Code Snippet
  1. namespace MyPaletteSet
  2. {
  3.     public interface IThePalette
  4.     {
  5.         ThePaletteSet PaletteSet { set; get; }
  6.         string PaletteName { get; }
  7.         void Close();
  8.         void ClosePaletteSet();
  9.     }
  10. }

Second code file is the custom PaletteSet, derived from Autodesk.AutoCAD.Windows.PaletteSet. As aforementioned, it is only possible when you use AutoCAD 2009 or later.

Here is the code:

Code Snippet
  1. using System;
  2. using System.Drawing;
  3. using System.Collections.Generic;
  4.  
  5. using Autodesk.AutoCAD.Windows;
  6. using Autodesk.AutoCAD.ApplicationServices;
  7.  
  8. namespace MyPaletteSet
  9. {
  10.     public class ThePaletteSet : PaletteSet
  11.     {
  12.         private static Guid _guid =
  13.             new Guid("B9169E25-3EC1-442F-B518-46B2DA174A2F");
  14.         private DocumentCollection _dwgManager = null;
  15.         private List<IThePalette> _paltettes = new List<IThePalette>();
  16.  
  17.         public event DocumentCollectionEventHandler DwgBecameCurrent;
  18.  
  19.         public ThePaletteSet()
  20.             : base("ThePaletteSet",null, _guid)
  21.         {
  22.             this.Style = PaletteSetStyles.ShowAutoHideButton |
  23.                 PaletteSetStyles.ShowCloseButton |
  24.                 PaletteSetStyles.Snappable;
  25.  
  26.             this.Opacity = 100;
  27.             this.Dock = DockSides.None;
  28.             this.DockEnabled = DockSides.None;
  29.  
  30.             this.Size = new Size(500, 400);
  31.             this.MinimumSize = new Size(250, 200);
  32.  
  33.             _dwgManager = Autodesk.AutoCAD.ApplicationServices
  34.                 .Application.DocumentManager;
  35.  
  36.           //Handle DocumentCollection events to bubble up the event for IThePalette
  37.             _dwgManager.DocumentBecameCurrent +=
  38.                 new DocumentCollectionEventHandler(_dwgManager_DocumentBecameCurrent);
  39.         }
  40.  
  41.         private void _dwgManager_DocumentBecameCurrent(
  42.             object sender, DocumentCollectionEventArgs e)
  43.         {
  44.             if (DwgBecameCurrent != null)
  45.             {
  46.                 DwgBecameCurrent(this, e);
  47.             }
  48.         }
  49.  
  50.         public void AddPalette(IThePalette palette)
  51.         {
  52.             bool exists = false;
  53.             foreach (IThePalette plt in _paltettes)
  54.             {
  55.                 if (plt.PaletteName.ToUpper() == palette.PaletteName.ToUpper())
  56.                 {
  57.                     exists = true;
  58.                     break;
  59.                 }
  60.             }
  61.  
  62.             if (!exists)
  63.             {
  64.                 System.Windows.Forms.Control ctl =
  65.                     palette as System.Windows.Forms.Control;
  66.  
  67.                 //Add to paletteset
  68.                 this.Add(palette.PaletteName, ctl);
  69.  
  70.                 _paltettes.Add(palette);
  71.                 palette.PaletteSet = this;
  72.             }
  73.         }
  74.  
  75.         public void RemovePalette(string paletteName)
  76.         {
  77.             if (_paltettes.Count == 0) return;
  78.  
  79.             for (int i = 0; i < _paltettes.Count; i++)
  80.             {
  81.                 if (_paltettes[i].PaletteName.ToUpper() == paletteName.ToUpper())
  82.                 {
  83.                     System.Windows.Forms.Control ctl =
  84.                         _paltettes[i] as System.Windows.Forms.Control;
  85.  
  86.                     this.Remove(i);
  87.                     _paltettes.RemoveAt(i);
  88.  
  89.                     ctl.Dispose();
  90.  
  91.                     if (_paltettes.Count == 0) this.Visible = false;
  92.  
  93.                     return;
  94.                 }
  95.             }
  96.         }
  97.  
  98.         public void ActivatePalette(string paletteName)
  99.         {
  100.             if (_paltettes.Count == 0) return;
  101.  
  102.             for (int i = 0; i < _paltettes.Count; i++)
  103.             {
  104.                 if (_paltettes[i].PaletteName.ToUpper() == paletteName.ToUpper())
  105.                 {
  106.                     this.Activate(i);
  107.                     return;
  108.                 }
  109.             }
  110.         }
  111.     }
  112. }

Pay attention to this line of code:

public event DocumentCollectionEventHandler DwgBecameCurrent;

and this line of code:

_dwgManager.DocumentBecameCurrent += new .......

Some of your Palettes may be designed to handle drawing based data. Since PaletteSet is a floating/modeless window, when current drawing changed in AutoCAD, the data shown in certain palette should be refreshed because of current drawing change (like AutoCAD's "Properties" window). Therefore, the this type of palette must be able to handle various events originally raised by DocumentCollection. So, here I simply handle the DocumentCollection events in the custom PaletteSet and bubble the events up. It is up to the individual Palette to subscribe the events when necessary. To simplfy the example, I only handle and raise the DocumentBecameCurrent event. In real production code, we could bubble up all the DocumentCollection events. The third code file is contains a static help method to create an instance of the custom PaletteSet. Here is the code:

Code Snippet
  1. using Autodesk.AutoCAD.Runtime;
  2.  
  3. [assembly: ExtensionApplication(typeof(MyPaletteSet.ThePaletteSetInitializer))]
  4.  
  5. namespace MyPaletteSet
  6. {
  7.     public class ThePaletteSetInitializer : IExtensionApplication
  8.     {
  9.         private static ThePaletteSet _paletteSet = null;
  10.  
  11.         public void Initialize()
  12.         {
  13.             //If necessary, add some code
  14.         }
  15.  
  16.         public void Terminate()
  17.         {
  18.             if (_paletteSet != null) _paletteSet.Dispose();
  19.         }
  20.  
  21.         public static ThePaletteSet CreateThePaltetteSet(IThePalette palette)
  22.         {
  23.             if (_paletteSet == null)
  24.             {
  25.                 _paletteSet = new ThePaletteSet();
  26.             }
  27.  
  28.             //Add palette to the PaletteSet
  29.             _paletteSet.AddPalette(palette);
  30.  
  31.             return _paletteSet;
  32.         }
  33.  
  34.         public static ThePaletteSet CreateThePaltetteSet()
  35.         {
  36.             if (_paletteSet == null)
  37.             {
  38.                 _paletteSet = new ThePaletteSet();
  39.             }
  40.  
  41.             return _paletteSet;
  42.         }
  43.     }
  44. }

That's it. Now we have a generic, pluggable PaletteSet. Build the project. From now on, when you want to build a suite of your own tools that have UI to be hosted in a PaletteSet, you can focus to the development of the Palette (Win Form UserControl) and never need to update the PaletteSet host and redeploy it. Let's see a couple of sample palettes. Sample 1: FirstPalette. Start a new class library command. It can be in the same solution as the "MyPaletteSet". But to better understand the "pluggable" nature, it is recommended to do this project in different solution. This will show that you can really focus on developing your Palette without having to update the PaletteSet at all. Once the project created, set reference to the DLL generated in "MyPaletteSet" project (MyPaletteSet.dll). Add a Win Form UserControl, called FirstPalette. It looks like:

Here is the code of the UserControl:

Code Snippet
  1. using System;
  2. using System.Windows.Forms;
  3. using Autodesk.AutoCAD.ApplicationServices;
  4. using MyPaletteSet;
  5.  
  6. namespace FirstPaletteTool
  7. {
  8.     public partial class FirstPalette : UserControl, IThePalette
  9.     {
  10.         private ThePaletteSet _parent = null;
  11.  
  12.         public FirstPalette()
  13.         {
  14.             InitializeComponent();
  15.  
  16.             Document dwg = Autodesk.AutoCAD.ApplicationServices.
  17.                 Application.DocumentManager.MdiActiveDocument;
  18.             if (dwg != null) txtFileName.Text = dwg.Name;
  19.         }
  20.  
  21.         #region IThePalette Members
  22.  
  23.         public string PaletteName
  24.         {
  25.             get { return "First Palette"; }
  26.         }
  27.  
  28.         public ThePaletteSet PaletteSet
  29.         {
  30.             get
  31.             {
  32.                 return _parent;
  33.             }
  34.             set
  35.             {
  36.                 _parent = value;
  37.                 _parent.DwgBecameCurrent +=
  38.                     new DocumentCollectionEventHandler(
  39.                         _parent_DwgBecameCurrent);
  40.             }
  41.         }
  42.  
  43.         void _parent_DwgBecameCurrent(object sender,
  44.             DocumentCollectionEventArgs e)
  45.         {
  46.             txtFileName.Text = e.Document.Name;
  47.         }
  48.  
  49.         public void Close()
  50.         {
  51.             if (_parent != null)
  52.             {
  53.                 _parent.RemovePalette(this.PaletteName);
  54.             }
  55.         }
  56.  
  57.         public void ClosePaletteSet()
  58.         {
  59.             if (_parent != null) _parent.Visible = false;
  60.         }
  61.  
  62.         #endregion
  63.  
  64.         private void button2_Click(object sender, EventArgs e)
  65.         {
  66.             this.ClosePaletteSet();
  67.         }
  68.  
  69.         private void button1_Click(object sender, EventArgs e)
  70.         {
  71.             this.Close();
  72.         }
  73.     }
  74. }

Then add another class file into the project: FirstPaletteCommand. Here is the code:

Code Snippet
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.Runtime;
  3.  
  4. using MyPaletteSet;
  5.  
  6. [assembly: CommandClass(typeof(FirstPaletteTool.FirstPaletteCommand))]
  7.  
  8. namespace FirstPaletteTool
  9. {
  10.     public class FirstPaletteCommand
  11.     {
  12.         private static ThePaletteSet _pltSet;
  13.  
  14.         [CommandMethod("StartFirst", CommandFlags.Session)]
  15.         public static void RunThisMethod()
  16.         {
  17.             Document dwg = Application.DocumentManager.MdiActiveDocument;
  18.  
  19.             try
  20.             {
  21.                 FirstPalette plt = new FirstPalette();
  22.  
  23.                 _pltSet = MyPaletteSet.
  24.                     ThePaletteSetInitializer.CreateThePaltetteSet(plt);
  25.                 _pltSet.Visible = true;
  26.                 _pltSet.ActivatePalette(plt.PaletteName);
  27.             }
  28.             catch(System.Exception ex)
  29.             {
  30.                 dwg.Editor.WriteMessage("\nError: " + ex.Message);
  31.             }
  32.         }
  33.     }
  34. }

Since I want this palette to show per-drawing based data, thus, I make this palette subscribe event "DwgBecameCurrent" raised by the hosting PaletteSet (MyPaletteSet). As you can see, no matter what UI components you place onto this palette (UserControl) and what code logic you will let this palette to execute, you can plug the palette into a common PaletteSet easily. New, let me create another palette: SecondPalette. Start a new class library project, called "SecondPaletteTool", set reference to "MyPaletteSet.dll". Add a Win Form UserControl, which looks like:


Its code is here:

Code Snippet
  1. using System;
  2. using System.Windows.Forms;
  3.  
  4. using MyPaletteSet;
  5.  
  6. namespace SecondPaletteTool
  7. {
  8.     public partial class SecondPalette : UserControl, IThePalette
  9.     {
  10.         private ThePaletteSet _parent = null;
  11.  
  12.         public SecondPalette()
  13.         {
  14.             InitializeComponent();
  15.         }
  16.  
  17.         #region IThePalette Members
  18.  
  19.         public ThePaletteSet PaletteSet
  20.         {
  21.             get
  22.             {
  23.                 return _parent;
  24.             }
  25.             set
  26.             {
  27.                 _parent = value;
  28.             }
  29.         }
  30.  
  31.         public string PaletteName
  32.         {
  33.             get { return "Second Palette"; }
  34.         }
  35.  
  36.         public void Close()
  37.         {
  38.             if (_parent != null)
  39.             {
  40.                 _parent.RemovePalette(this.PaletteName);
  41.             }
  42.         }
  43.  
  44.         public void ClosePaletteSet()
  45.         {
  46.             if (_parent != null) _parent.Visible = false;
  47.         }
  48.  
  49.         #endregion
  50.  
  51.         private void button1_Click(object sender, EventArgs e)
  52.         {
  53.             this.Close();
  54.         }
  55.  
  56.         private void button2_Click(object sender, EventArgs e)
  57.         {
  58.             this.ClosePaletteSet();
  59.         }
  60.     }
  61. }

Notice that this palette does not subscribe event raised by hosting PaletteSet. Add a class into the project: SecondPaletteCommand:

Code Snippet
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.Runtime;
  3.  
  4. using MyPaletteSet;
  5.  
  6. [assembly: CommandClass(typeof(SecondPaletteTool.SecondPaletteCommand))]
  7.  
  8. namespace SecondPaletteTool
  9. {
  10.     public class SecondPaletteCommand
  11.     {
  12.         private static ThePaletteSet _pltSet;
  13.  
  14.         [CommandMethod("StartSecond", CommandFlags.Session)]
  15.         public static void RunThisMethod()
  16.         {
  17.             Document dwg = Application.DocumentManager.MdiActiveDocument;
  18.  
  19.             try
  20.             {
  21.                 SecondPalette plt = new SecondPalette();
  22.  
  23.                 _pltSet = MyPaletteSet.
  24.                     ThePaletteSetInitializer.CreateThePaltetteSet(plt);
  25.                 _pltSet.Visible = true;
  26.                 _pltSet.ActivatePalette(plt.PaletteName);
  27.             }
  28.             catch (System.Exception ex)
  29.             {
  30.                 dwg.Editor.WriteMessage("\nError: " + ex.Message);
  31.             }
  32.         }
  33.     }
  34. }

As you can see, no matter how different the second palette from the first one, it can be plugged into MyPaletteSet in the same way, because, to MyPaletteSet, these 2 palettes are the same type: IMyPalette.

Now, start AutoCAD and "NETLOAD" the 2 palette projects (e.g. load FirstPaletteTool.dll and SecondPaletteTool.dll separately, as if the 2 DLLs are deployed and loaded separately). Enter command "StartFirst" and/or "StartSecond". You can see the corresponding palette will be shown in a common PaletteSet. If you open more than one drawings in AutoCAD and switch the active drawing, you can see the file name shown on the "First Palette" changes accordingly, because this palette handles DwgBecameCurrent event raised by the hosting PaletteSet.

From now on, whenever I want to develop a new AutoCAD tool that would have a modeless window as UI, I can go ahead to develop the UI as Win Form UserControl. Once it is done, I can simply plug it into the common hosting PaletteSet. Since the newly developed palette is in its own project, it can be deployed independently to all existing palettes, yet they are hosted in the same PaletteSet.

In the FirstPalette, I have to add "using Autodesk.AutoCAD.ApplicationServices;" because the palette has to comsume DocumentCollectionEventArgs in the DwgBecameCurrent event handler. In real development code, it is best practice to not let the UserControl to be tied to AutoCAD's dll. In this case, it is better to define my own custom EvenArgs and my own custom EventHandler in the MyPaletteSet project and use them to bubble up the various DocumentCollection events.

Update note: due to the original posted code format, a key part of the code did not show correctly, which were the topic of the comments below. Now I updated the code posting format and show the line of code in red, which was previously shown wrong. - Norman, 2011-01-03.

10 comments:

  1. Pluggable PaletteSet

    A practical time saving concept! Thanks.

    But I encountered the follow error at the following line and it could be due to me overlooking something:

    public class ThePaletteSet : PaletteSet
    {

    private List _paltettes = new List();

    Error Description
    Using the generic type 'System.Collections.Generic.List' requires '1' type arguments

    What works is something along the following line:

    class BasicCollection : IEnumerable
    {
    private List data = new List();

    Maybe you could explain this?

    ReplyDelete
  2. OK, this is not due to the blog's formatting issue that cause the "<>" not being shown correctly. I mentioned this in my later post and I have changed the way how code is post.

    This this case, that line of code should be

    private List _palettes=new List();

    ReplyDelete
  3. Well, it is still not shown correctly. Here is VB.NET equivalent:

    Private _palettes As New List(of IThePalette)

    Because VB.NET use List(of...), not like C# using angle bracket, so this blog format can show it correctly.

    ReplyDelete
  4. I am unable to build the generic pluggable PaletteSet in either VB or C# even after making the corrections posted so far. For the C# only the code in the blog was entered.

    In VB the errors are:


    Error 1 'Public Event DocumentBecameCurrent(sender As Object, e As Autodesk.AutoCAD.ApplicationServices.DocumentCollectionEventArgs)' is an event, and cannot be called directly. Use a 'RaiseEvent' statement to raise an event. U:\VS-Projects\Visual Studio 2010\Projects\MyPalletSet\MyPalletSet\CodeFile2.vb 37 13 MyPalletSet


    Error 2 Value of type 'System.Windows.Forms.Control' cannot be converted to 'System.Windows.Controls.Control'. U:\VS-Projects\Visual Studio 2010\Projects\MyPalletSet\MyPalletSet\CodeFile2.vb 54 38 MyPalletSet

    Error 3 Value of type 'System.Windows.Controls.Control' cannot be converted to 'System.Windows.Forms.Control'. U:\VS-Projects\Visual Studio 2010\Projects\MyPalletSet\MyPalletSet\CodeFile2.vb 57 45 MyPalletSet


    In C# the errors are:

    Error 1 The type or namespace name 'Drawing' does not exist in the namespace 'System' (are you missing an assembly reference?) U:\VS-Projects\Visual Studio 2010\Projects\PaletteSet\PaletteSet\CodeFile2.cs 2 14 PaletteSet

    Error 2 'Autodesk.AutoCAD.Windows.PaletteSet.Size' is a 'property' but is used like a 'type' U:\VS-Projects\Visual Studio 2010\Projects\PaletteSet\PaletteSet\CodeFile2.cs 31 29 PaletteSet

    Error 3 'Autodesk.AutoCAD.Windows.PaletteSet.Size' is a 'property' but is used like a 'type' U:\VS-Projects\Visual Studio 2010\Projects\PaletteSet\PaletteSet\CodeFile2.cs 32 36 PaletteSet

    ReplyDelete
  5. Hi,

    For the error in the C# project, you need to add reference to System.Drawing.dll

    As you may have noticed the previous comment, the blog formatting made the code for Generic type (List...) not readable, because the anglar bracket the T type is missing. I have changed the code post appraoch iin my later post, but have not updated older posts.

    ReplyDelete
  6. Hi aks,

    I have updated the code so they are now shown in correct format and easier to read/copy. HTH.

    ReplyDelete
  7. I work with architects and engineers on a daily basis. AutoCAD is definitely the industry standard for the construction industry. Other industries, such as shipbuilding, manufacturing (CAD/CAM), mechanical engineering, etc. may use different software designed for specific needs. It really depends on what you're looking to do, but you can't go wrong learning AutoCAD. However, it's not cheap software. If you're looking for something to just to mess around with, there are plenty of free CAD programs out there to get stated with (some are better than others).

    Solidworks 2012

    ReplyDelete
  8. Great code Norman!! I love it!! Can't wait to give this a try...thanks a lot.

    ReplyDelete
  9. NCCS - Over 20 years, we have offered CAD/CAM and manufacturing solutions to thousands of Australian and New Zealand companies.Service, quality and a reputation make up the core values of our company and employees at NCCS. We offer a superior product at affordable prices that deliver a consistent product to our customers day after day and year after year.

    ReplyDelete
  10. Thanks so much. I am beginner in the AutoCAD Api .Net and this post realy help me to implement a paletteSet depending on the document sucessfully

    ReplyDelete