Accueil
AutoLISP
.NET
Tutoriels
Liens
Contact

User interfaces with AutoCAD .NET

Introduction

Foreword

Command line


With Windows Forms (WinForms)

Modal dialog box

Modeless dialog box

Palette


With Windows Presentation Foundation (WPF)

Modal dialog box

Modal dialog box (Binding)

Palette with MVVM






Introduction



Foreword

We will not dwell on the usefulness (or uselessness) of what the examples do in AutoCAD. A simple task was deliberately chosen so as not to interfere too much with the purpose of transferring data between the different components of the program.
The task is simply to draw a circle on the specified point according to the layer and radius previously entered by the user.

Each example has a different user interface, gradually adding a bit of complexity compared to the previous example.

The examples assume, at a minimum, knowledge of the basics of .NET programming and do not dwell on the details of creating interfaces in Visual Studio, rather they focus on interactions between the interface and the application. .

The codes use some new features of C# 6 (Visual Studio 2015).



Command line

Let's start with the command line, basic interface to get user input.

Nothing special here, it's a simple AutoCAD command that uses objects derived from PromptEditorOptions and PromptResult to get user input.
Everything happens in the same class, even in the same method.

Note that AutoCAD commands are defined in a class and only one instance of this class is created for each document at the first call of a command (if it is defined by a non-static method). This makes it possible to use members of this class (field or properties) to store data per document as the default values of the layer and radius used by our command.

A private method: GetLayerNames() is used to collect the layer names of the active document to perform certain validations.

Commands.cs
using System.Collections.Generic;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Core.Application;

[assembly: CommandClass(typeof(AcadUISample.CommandLine.Commands))]

namespace AcadUISample.CommandLine
{
    public class Commands
    {
        // instance fields
        Document doc;  // active document
        double radius; // radius default value
        string layer;  // layer default value

        /// <summary>
        /// Creates a new instance of Commands.
        /// This constructor is run once per document
        /// at the first call of a 'CommandMethod' method.
        /// </summary>
        public Commands()
        {
            // private fields initialization (initial default values)
            doc = AcAp.DocumentManager.MdiActiveDocument;
            radius = 10.0;
            layer = (string)AcAp.GetSystemVariable("clayer");
        }

        /// <summary>
        /// Command to draw the circle.
        /// </summary>
        [CommandMethod("CMD_CIRCLE")]
        public void DrawCircleCmd()
        {
            var db = doc.Database;
            var ed = doc.Editor;

            // choose of the layer
            var layers = GetLayerNames(db);
            if (!layers.Contains(layer))
            {
                layer = (string)AcAp.GetSystemVariable("clayer");
            }
            var strOptions = new PromptStringOptions("\nLayer name: ");
            strOptions.DefaultValue = layer;
            strOptions.UseDefaultValue = true;
            var strResult = ed.GetString(strOptions);
            if (strResult.Status != PromptStatus.OK)
                return;
            if (!layers.Contains(strResult.StringResult))
            {
                ed.WriteMessage(
                  $"\nNone layer named '{strResult.StringResult}' in the layer table.");
                return;
            }
            layer = strResult.StringResult;

            // specification of the radius
            var distOptions = new PromptDistanceOptions("\nSpecify the radius: ");
            distOptions.DefaultValue = radius;
            distOptions.UseDefaultValue = true;
            var distResult = ed.GetDistance(distOptions);
            if (distResult.Status != PromptStatus.OK)
                return;
            radius = distResult.Value;

            // specification of the center
            var ppr = ed.GetPoint("\nSpecify the center: ");
            if (ppr.Status == PromptStatus.OK)
            {
                // drawing of the circle in the current space
                using (var tr = db.TransactionManager.StartTransaction())
                {
                    var curSpace =
                      (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
                    using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, radius))
                    {
                        circle.TransformBy(ed.CurrentUserCoordinateSystem);
                        circle.Layer = strResult.StringResult;
                        curSpace.AppendEntity(circle);
                        tr.AddNewlyCreatedDBObject(circle, true);
                    }
                    tr.Commit();
                }
            }
        }

        /// <summary>
        /// Gets the layer list.
        /// </summary>
        /// <param name="db">Database instance this method applies to.</param>
        /// <returns>Layer names list.</returns>
        private List<string> GetLayerNames(Database db)
        {
            var layers = new List<string>();
            using (var tr = db.TransactionManager.StartOpenCloseTransaction())
            {
                var layerTable = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);
                foreach (ObjectId id in layerTable)
                {
                    var layer = (LayerTableRecord)tr.GetObject(id, OpenMode.ForRead);
                    layers.Add(layer.Name);
                }
            }
            return layers;
        }
    }
}
        

Back to the summary.





With Windows Forms (WinForms)



Modal dialog box

The dialog boxes can be displayed in two modes: modal or modeless.

In AutoCAD, we display the modal dialogs with the method: Application.ShowModalDialog().
The user no longer has access to AutoCAD until the dialog box is hidden or closed.

The Application.ShowModalDialog() method requires an instance of the form to display as a parameter and returns an object of type DialogResult (this is an enumeration) that indicates how the dialog box was closed.
Typically, the values ​​DialogResult.OK and DialogResult.Cancel are assigned to the DialogResult property of two buttons: "OK" and "Cancel". So we see how the method that displays the dialog will be able to recover at the same time a value indicating if the user closed it by clicking on "OK" or "Cancel".

But in general, the project defines at least two classes: a class in which the AutoCAD command is defined to display the dialog box and to act in AutoCAD according to the result and another class in which the dialog itself is defined.
We have to be able to pass data from one class to another.

To pass the list of layers of the current drawing and the default values (layer and radius) of the command to the dialog box, we can use the parameters of the constructor of the form (we will overload it accordingly).
The list of layer names will be linked to the dropdown list of the form as a data source.

And to pass the values specified by the user in the dialog box to the command we can define properties in the class of the form whose values will be accessible via the instance of the form created in the command.

Commands.cs
using System.Collections.Generic;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

[assembly: CommandClass(typeof(AcadUISample.ModalDialog.Commands))]

namespace AcadUISample.ModalDialog
{
    public class Commands
    {
        // instance fields
        Document doc;  // active document
        double radius; // radius default value
        string layer;  // layer default value

        /// <summary>
        /// Crates a new instance of Commands.
        /// This constructor is run once per document
        /// at the first call of a 'CommandMethod' method.
        /// </summary>
        public Commands()
        {
            // private fields initialization (initial default values)
            doc = AcAp.DocumentManager.MdiActiveDocument;
            layer = (string)AcAp.GetSystemVariable("clayer");
            radius = 10.0;
        }

        /// <summary>
        /// Command to show the dialog box
        /// </summary>
        [CommandMethod("CMD_MODAL")]
        public void ModalDialogCmd()
        {
            // creation of an instance of ModalDialog
            // with the document data (layers and default values)
            var layers = GetLayerNames(doc.Database);
            if (!layers.Contains(layer))
            {
                layer = (string)AcAp.GetSystemVariable("clayer");
            }
            using (var dialog = new ModalDialog(layers, layer, radius))
            {
                // shows the dialog box in modal mode
                // and acts according to the DialogResult value
                var dlgResult = AcAp.ShowModalDialog(dialog);
                if (dlgResult == System.Windows.Forms.DialogResult.OK)
                {
                    // fields update
                    layer = dialog.Layer;
                    radius = dialog.Radius;
                    // circle drawing
                    DrawCircle(radius, layer);
                }
            }
        }

        /// <summary>
        /// Draws a circle.
        /// </summary>
        /// <param name="radius">Circle radius.</param>
        /// <param name="layer">Circle center.</param>
        private void DrawCircle(double radius, string layer)
        {
            var db = doc.Database;
            var ed = doc.Editor;
            var ppr = ed.GetPoint("\nSpecify the center: ");
            if (ppr.Status == PromptStatus.OK)
            {
                using (var tr = db.TransactionManager.StartTransaction())
                {
                    var curSpace = 
                        (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
                    using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, radius))
                    {
                        circle.TransformBy(ed.CurrentUserCoordinateSystem);
                        circle.Layer = layer;
                        curSpace.AppendEntity(circle);
                        tr.AddNewlyCreatedDBObject(circle, true);
                    }
                    tr.Commit();
                }
            }
        }

        /// <summary>
        /// Gets the layer list.
        /// </summary>
        /// <param name="db">Database instance this method applies to.</param>
        /// <returns>Layer names list.</returns>
        private List<string> GetLayerNames(Database db)
        {
            var layers = new List<string>();
            using (var tr = db.TransactionManager.StartOpenCloseTransaction())
            {
                var layerTable = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);
                foreach (ObjectId id in layerTable)
                {
                    var layer = (LayerTableRecord)tr.GetObject(id, OpenMode.ForRead);
                    layers.Add(layer.Name);
                }
            }
            return layers;
        }
    }
}
            
ModalDialog.cs
using System.Collections.Generic;
using System.Windows.Forms;

using Autodesk.AutoCAD.EditorInput;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

namespace AcadUISample.ModalDialog
{
    public partial class ModalDialog : Form
    {
        double radius;

        /// <summary>
        /// Gets the radius value.
        /// </summary>
        public double Radius => radius;

        /// <summary>
        /// Gets the selected item in the ComboBox control.
        /// </summary>
        public string Layer => (string)cbxLayer.SelectedItem;

        /// <summary>
        /// Creates a new instance of ModalDialog.
        /// </summary>
        /// <param name="layers">Collection of layers to be bound to the ComboBox control.</param>
        /// <param name="clayer">Default layer name.</param>
        /// <param name="radius">Default radius.</param>
        public ModalDialog(List<string> layers, string clayer, double radius)
        {
            InitializeComponent();

            // assigning the DialogResult values to the buttons
            btnCancel.DialogResult = DialogResult.Cancel;
            btnOk.DialogResult = DialogResult.OK;

            // binding the layer collection to the ComboBox control
            cbxLayer.DataSource = layers;
            cbxLayer.SelectedItem = clayer;

            // radius default value
            txtRadius.Text = radius.ToString();
        }

        /// <summary>
        /// Handles the 'TextChanged' event of the 'Radius' TextBox.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event data.</param>
        private void txtRadius_TextChanged(object sender, System.EventArgs e)
        {
            // OK button is 'disable' if the text does not represent a valid number
            // the radius field is updated accordingly
            btnOk.Enabled = double.TryParse(txtRadius.Text, out radius);
        }

        /// <summary>
        /// Handles the 'Click' event of the 'Radius' button.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event data.</param>
        private void btnRadius_Click(object sender, System.EventArgs e)
        {
            var ed = AcAp.DocumentManager.MdiActiveDocument.Editor;
            var opts = new PromptDistanceOptions("\nSpecify the radius: ");
            opts.AllowNegative = false;
            opts.AllowZero = false;
            // AutoCAD automatically hides the dialog box to let the user specify the radius
            var pdr = ed.GetDistance(opts);
            if (pdr.Status == PromptStatus.OK)
                txtRadius.Text = pdr.Value.ToString();
        }
    }
}
            

Back to the summary.




Modeless dialog box

Unlike modal dialog boxes, modeless dialog boxes do not monopolize the focus when they are displayed.
They take focus automatically when the cursor enters their area, but do not automatically return it to AutoCAD when the cursor comes out.
In addition, they run in the context of the AutoCAD application, unlike the modal boxes that run in the context of the active document.
It is therefore up to the program to take care of certain passages of focus, to hide the box if necessary, to lock the active document before being able to modify it and also to manage any changes to the data related to the interface in the document as well as active document changes.

For our example, this last point is important for the list of layers that can be modified while the dialog box is displayed or to be different from one document to another.
AutoCAD provides the UIBindings API, which is used to link AutoCAD data with user interfaces, including some collections such as layers.
The DataItemCollection collections obtained with UIBindings are said to be "observable", they implement several interfaces including INotitfyCollectionChanged which, as its name suggests, notifies changes that have occurred in the collection to the user interface to which it is linked. Of course we could have used this API also with the modal dialog.

To display a modeless dialog in AutoCAD, we use the Application.ShowModelessDialog () method which requires a form as argument, but in this case, nothing returns (void).
With this type of dialog box, the command can only display the box, it is the event managers of the various controls that will interact with AutoCAD.

Commands.cs
using Autodesk.AutoCAD.Runtime;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

[assembly: CommandClass(typeof(AcadUISample.Modeless.Commands))]

namespace AcadUISample.Modeless
{
    public class Commands
    {
        /// <summary>
        /// Command to show the dialog box
        /// </summary
        [CommandMethod("CMD_MODELESS")]
        public static void ShowModelessDialog()
        {
            // create a new instance of ModelessDialog
            var dialog = new ModelessDialog();

            // show the dialog box in modeless mode
            AcAp.ShowModelessDialog(dialog);
        }
    }
}
            
ModelessDialog.cs
using System;
using System.Linq;
using System.Windows.Forms;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Windows.Data;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

namespace AcadUISample.Modeless
{
    public partial class ModelessDialog : Form
    {
        // private fields
        double radius;              // circle radius
        DataItemCollection layers;  // collection of layer data

        /// <summary>
        /// Creates a new instance of ModelessDialog.
        /// Defines the data bindings for the ComboBox control.
        /// </summary>
        public ModelessDialog()
        {
            InitializeComponent();

            layers = AcAp.UIBindings.Collections.Layers;

            // binding the layer data to the ComboBox control
            BindData();

            // updating the ComboBox contol when the layer data collection changes
            layers.CollectionChanged += (s, e) => BindData();

            // radius default value
            txtRadius.Text = "10";
        }

        /// <summary>
        /// Handles the 'TextChanged' event of the 'Radius' TextBox.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event data.</param>
        private void txtRadius_TextChanged(object sender, EventArgs e)
        {
            // OK button is 'disable' if the text does not represent a valid number
            // the radius field is updated accordingly
            btnOk.Enabled = double.TryParse(txtRadius.Text, out radius);
        }

        /// <summary>
        /// Handles the 'Click' event of the 'Radius' button.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event data.</param>
        private void btnRadius_Click(object sender, EventArgs e)
        {
            // hide the dialog box
            this.Hide();

            // prompt the user to specify a distance
            var ed = AcAp.DocumentManager.MdiActiveDocument.Editor;
            var opts = new PromptDistanceOptions("\nSpecify the radius: ");
            opts.AllowNegative = false;
            opts.AllowZero = false;
            var pdr = ed.GetDistance(opts);
            if (pdr.Status == PromptStatus.OK)
            {
                txtRadius.Text = pdr.Value.ToString();
            }

            // show the dialog box
            this.Show();
        }

        /// <summary>
        /// Handles the 'Click' event of the 'OK' button.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event data.</param>
        private void btnOk_Click(object sender, EventArgs e)
        {
            var doc = AcAp.DocumentManager.MdiActiveDocument;
            if (doc != null)
            {
                // set the focus to AutoCAD editor
                // before AutoCAD 2015, use : Autodesk.AutoCAD.Internal.Utils.SetFocusToDwgView();
                AcAp.MainWindow.Focus();

                // lock the document
                using (doc.LockDocument())
                {
                    // draw the circle
                    DrawCircle(doc, radius, (string)cbxLayer.SelectedItem);
                }
            }
        }

        /// <summary>
        /// Draws a circle.
        /// </summary>
        /// <param name="doc">Document instance the method applie to.</param>
        /// <param name="radius">Circle radius.</param>
        /// <param name="layer">Circle center.</param>
        private void DrawCircle(Document doc, double radius, string layer)
        {
            var db = doc.Database;
            var ed = doc.Editor;
            var ppr = ed.GetPoint("\nSpecify the center: ");
            if (ppr.Status == PromptStatus.OK)
            {
                using (var tr = db.TransactionManager.StartTransaction())
                {
                    var curSpace = 
                        (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
                    using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, radius))
                    {
                        circle.TransformBy(ed.CurrentUserCoordinateSystem);
                        circle.Layer = layer;
                        curSpace.AppendEntity(circle);
                        tr.AddNewlyCreatedDBObject(circle, true);
                    }
                    tr.Commit();
                }
            }
        }

        /// <summary>
        /// Defines the data bindings of the Combobox control.
        /// </summary>
        private void BindData()
        {
            // binding to the data source
            // the DataItemCollection is converted into a layer names array.
            cbxLayer.DataSource = layers.Select(x => ((INamedValue)x).Name).ToArray();

            // selection of the current layer
            cbxLayer.SelectedItem = ((INamedValue)layers.CurrentItem)?.Name;
        }
    }
}
            

Back to the summary.




Palette

The dialog boxes as above are not (more) used by AutoCAD which prefers the palettes (or more precisely the palette sets: PaletteSet).
The previous example is mainly used to illustrate the management of a modeless form.

A palette (or a set of palettes) is a modeless, dockable and collapsible interface that can contain several tabs (the palettes themselves).
This is an AutoCAD-specific object whose creation is a little less simple than that of a Windows form.

As with Windows forms, it is customary to create a class that derives from PaletteSet to define the appearance and controls contained in the palette set.

Each tab in the palette is an instance of System.Windows.Forms.UserControl that is added to the palette set.

As with the previous example, the layer collection is linked to an instance of the DataItemCollection, but each item in the drop-down list is "drawn" to display a filled square of the layer color next to the layer name. Again, this could have been done with the previous examples as well.

To simplify (and secure) access to the current document, a command similar to that defined in the "Command Line" example is called from the event handler of the OK button via the SendStringToExecute() method. This allows AutoCAD to lock the active document and pass the focus to it.
As a bonus, the command can be re-called by Enter.

Commands.cs
using Autodesk.AutoCAD.ApplicationServices.Core;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;

[assembly: CommandClass(typeof(AcadUISample.Palette.Commands))]

namespace AcadUISample.Palette
{
    public class Commands
    {
        // static field
        static CustomPaletteSet palette;

        // instance fields (default values)
        double radius = 10.0;
        string layer;

        /// <summary>
        /// Command to show the palette.
        /// </summary>
        [CommandMethod("CMD_PALETTE")]
        public void ShowPaletteSet()
        {
            // creation of the palette at the first call of the command
            if (palette == null)
                palette = new CustomPaletteSet();
            palette.Visible = true;
        }

        /// <summary>
        /// Command to draw the circle
        /// </summary>
        [CommandMethod("CMD_CIRCLE_PALETTE", CommandFlags.Modal)]
        public void DrawCircleCmd()
        {
            var doc = Application.DocumentManager.MdiActiveDocument;
            var db = doc.Database;
            var ed = doc.Editor;

            // choosethe layer
            if (string.IsNullOrEmpty(layer))
                layer = (string)Application.GetSystemVariable("clayer");
            var strOptions = new PromptStringOptions("\nLayer name: ");
            strOptions.DefaultValue = layer;
            strOptions.UseDefaultValue = true;
            var strResult = ed.GetString(strOptions);
            if (strResult.Status != PromptStatus.OK)
                return;
            layer = strResult.StringResult;

            // specify the radius
            var distOptions = new PromptDistanceOptions("\nSpecify the radius: ");
            distOptions.DefaultValue = radius;
            distOptions.UseDefaultValue = true;
            var distResult = ed.GetDistance(distOptions);
            if (distResult.Status != PromptStatus.OK)
                return;
            radius = distResult.Value;

            // specify the center
            var ppr = ed.GetPoint("\nSpecify the center: ");
            if (ppr.Status == PromptStatus.OK)
            {
                // drawing the circle in the current space
                using (var tr = db.TransactionManager.StartTransaction())
                {
                    var curSpace = 
                        (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
                    var ucs = ed.CurrentUserCoordinateSystem;
                    using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, distResult.Value))
                    {
                        circle.TransformBy(ed.CurrentUserCoordinateSystem);
                        circle.Layer = strResult.StringResult;
                        curSpace.AppendEntity(circle);
                        tr.AddNewlyCreatedDBObject(circle, true);
                    }
                    tr.Commit();
                }
            }
        }
    }
}
            
CustomPaletteSet.cs
using System;

using Autodesk.AutoCAD.ApplicationServices.Core;
using Autodesk.AutoCAD.Windows;

namespace AcadUISample.Palette
{
    internal class CustomPaletteSet : PaletteSet
    {
        // static field
        static bool wasVisible;

        /// <summary>
        /// Creates a new instance of CustomPaletteSet.
        /// </summary>
        public CustomPaletteSet()
            : base("Palette", "CMD_PALETTE", new Guid("{1836C7AC-C70E-4CF7-AA05-F6298D275046}"))
        {
            Style =
                PaletteSetStyles.ShowAutoHideButton |
                PaletteSetStyles.ShowCloseButton |
                PaletteSetStyles.ShowPropertiesMenu;
            MinimumSize = new System.Drawing.Size(250, 150);
            Add("Circle", new PaletteTab());

            // automatically hide the palette while none document is active (no document state)
            var docs = Application.DocumentManager;
            docs.DocumentBecameCurrent += (s, e) => Visible = e.Document == null ? false : wasVisible;
            docs.DocumentCreated += (s, e) => Visible = wasVisible;
            docs.DocumentToBeDeactivated += (s, e) => wasVisible = Visible;
            docs.DocumentToBeDestroyed += (s, e) =>
            {
                wasVisible = Visible;
                if (docs.Count == 1)
                    Visible = false;
            };
        }
    }
}
      
PaletteTab.cs
using System;
using System.Drawing;
using System.Windows.Forms;

using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Windows.Data;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

namespace AcadUISample.Palette
{
    /// <summary>
    /// Describes the tab (palette) of a palette set (PaletteSet)
    /// </summary>
    public partial class PaletteTab : UserControl
    {
        // instance fields
        double radius;
        DataItemCollection layers; // layer data collection

        /// <summary>
        /// Creates a new instance of PaletteTab.
        /// Defines the data bindings for the ComboBox control.
        /// </summary>
        public PaletteTab()
        {
            InitializeComponent();

            layers = layers = AcAp.UIBindings.Collections.Layers; ;

            // binds the layer data to the ComboBox control
            BindData();

            // updates the ComboBox control when the layer data collection changes
            layers.CollectionChanged += (s, e) => BindData();

            // radius default value
            txtRadius.Text = "10";
        }

        /// <summary>
        /// Handles the 'TextChanged' event of the 'Radius' TextBox.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event data.</param>
        private void txtRadius_TextChanged(object sender, EventArgs e)
        {
            // OK button is 'disable' if the text does not represent a valid number
            // the radius field is updated accordingly
            btnOk.Enabled = double.TryParse(txtRadius.Text, out radius);
        }

        // <summary>
        /// Handles the 'Click' event of the 'Radius' button.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event data.</param>
        private void btnRadius_Click(object sender, EventArgs e)
        {
            // set the focus to AutoCAD editor
            // before AutoCAD 2015, use : Autodesk.AutoCAD.Internal.Utils.SetFocusToDwgView();
            AcAp.MainWindow.Focus();

            // prompt the user to specify a distance
            var ed = AcAp.DocumentManager.MdiActiveDocument.Editor;
            var opts = new PromptDistanceOptions("\nSpecify the radius: ");
            opts.AllowNegative = false;
            opts.AllowZero = false;
            var pdr = ed.GetDistance(opts);
            if (pdr.Status == PromptStatus.OK)
                txtRadius.Text = pdr.Value.ToString();
        }

        /// <summary>
        /// Handles the 'Click' event of the 'OK' button.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event data.</param>
        private void btnOk_Click(object sender, EventArgs e)
        {
            // calling the 'CMD_CIRCLE' command with the layer and radius
            AcAp.DocumentManager.MdiActiveDocument?.SendStringToExecute(
               $"CMD_CIRCLE_PALETTE \"{((INamedValue)cbxLayer.SelectedItem).Name}\" {radius} ", false, false, false);
        }

        /// <summary>
        /// Defines the data bindings of the Combobox control.
        /// </summary>
        private void BindData()
        {
            // binding to data source
            cbxLayer.DataSource = new BindingSource(layers, null);

            // definition of the drop down lsit items appearance
            cbxLayer.DrawMode = DrawMode.OwnerDrawFixed;
            cbxLayer.DrawItem += (_, e) =>
            {
                if (e.Index > -1)
                {
                    // get the item and its properties
                    var item = layers[e.Index];
                    var properties = item.GetProperties();

                    // draw a square with the layer color
                    var color = (Autodesk.AutoCAD.Colors.Color)properties["Color"].GetValue(item);
                    var bounds = e.Bounds;
                    int height = bounds.Height;
                    Graphics graphics = e.Graphics;
                    e.DrawBackground();
                    var rect = new Rectangle(bounds.Left + 4, bounds.Top + 4, height - 8, height - 8);
                    graphics.FillRectangle(new SolidBrush(color.ColorValue), rect);
                    graphics.DrawRectangle(new Pen(Color.Black), rect);

                    // write the layer name
                    graphics.DrawString(
                        (string)properties["Name"].GetValue(item),
                        e.Font,
                        new SolidBrush(e.ForeColor), bounds.Left + height, bounds.Top + 1);
                    e.DrawFocusRectangle();
                }
            };

            // select the current layer
            cbxLayer.SelectedItem = layers.CurrentItem;
        }
    }
}
            

Back to the summary.





With Windows Presentation Foundation (WPF)


All of the above examples use WinForms technology.
WPF (Windows Presentation Foundation) is the "new technology" for creating graphical interfaces.
The use of the XAML language makes it possible to create rich graphical interfaces a little like HTML.

A minimal WPF interface is defined using two files:
- the .xaml file which defines the appearance of the interface, the events subscriptions and the data bindings of the controls;
- the .xaml.cs file which defines the interaction logic between the user interface and the application (code behind).

By default, unless you are in a WPF Application type project, Visual Studio 2015 does not offer a "Window (WPF)", as an element to add. But we can always add a "User Control (WPF)" and replace UserControl by Window in the .xaml and .xaml.cs files.
You can also add a "Title" attribute and some others to the Window tag.
At this point, you can "Export Template" (File menu) as "Item Template" for Visual Studio to offer in the future.


Modal dialog box

As an appetizer, let's start with a simple modal dialog box equivalent to that of the previous modal dialog box (WinForm).

The main difference is the use of the XAML language for the description of the user interface whose architecture looks a little like that of HTML (the comparison stops there).
The controls are named here to access them from code behind. Control events are subscribed to event handlers located in the code behind.

To display a WPF modal dialog box from AutoCAD, we use the Application.ShowModalWindow() method by passing it an instance of System.Windows.Window created with an overloaded constructor similar to the one used with WinForms.

Commands.cs
using System.Collections.Generic;
using System.Linq;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

[assembly: CommandClass(typeof(AcadUISample.ModalWpf.Commands))]

namespace AcadUISample.ModalWpf
{
    public class Commands
    {
        // instance fields
        Document doc;
        Database db;
        Editor ed;
        double radius; // radius default value
        string layer;  // layer default value

        /// <summary>
        /// Crates a new instance of Commands.
        /// This constructor is run once per document
        /// at the first call of a 'CommandMethod' method.
        /// </summary>
        public Commands()
        {
            // private fields initialization (initial default values)
            doc = AcAp.DocumentManager.MdiActiveDocument;
            db = doc.Database;
            ed = doc.Editor;
            // initial default values
            layer = (string)AcAp.GetSystemVariable("clayer");
            radius = 10.0;
        }

        /// <summary>
        /// Command to show the dialog box
        /// </summary>
        [CommandMethod("CMD_MODAL_WPF")]
        public void ModalWpfDialogCmd()
        {
            var layers = GetLayerNames();
            if (!layers.Contains(layer))
            {
                layer = (string)AcAp.GetSystemVariable("clayer");
            }

            // shows the dialog box
            var dialog = new ModalWpfDialog(layers, layer, radius);
            var result = AcAp.ShowModalWindow(dialog);
            if (result.Value)
            {
                // fields update
                layer = dialog.Layer;
                radius = dialog.Radius;

                // circle drawing
                var ppr = ed.GetPoint("\nSpecify the center: ");
                if (ppr.Status == PromptStatus.OK)
                {
                    // drawing the circlr in current space
                    using (var tr = db.TransactionManager.StartTransaction())
                    {
                        var curSpace = 
                            (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
                        using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, radius))
                        {
                            circle.TransformBy(ed.CurrentUserCoordinateSystem);
                            circle.Layer = layer;
                            curSpace.AppendEntity(circle);
                            tr.AddNewlyCreatedDBObject(circle, true);
                        }
                        tr.Commit();
                    }
                }
            }
        }

        /// <summary>
        /// Gets the layer list.
        /// </summary>
        /// <param name="db">Database instance this method applies to.</param>
        /// <returns>Layer names list.</returns>
        private List<string> GetLayerNames()
        {
            using (var tr = db.TransactionManager.StartOpenCloseTransaction())
            {
                return ((LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead))
                    .Cast<ObjectId>()
                    .Select(id => ((LayerTableRecord)tr.GetObject(id, OpenMode.ForRead)).Name)
                    .ToList();
            }
        }
    }
}
            
ModalWpfDialog.xaml
<Window x:Class="AcadUISample.ModalWpf.ModalWpfDialog"
        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:AcadUISample.ModalWpf"
        mc:Ignorable="d" 
        Title="ModalWpfDialog" 
        Height="160" Width="300" 
        MinHeight="160" MinWidth="250"
        WindowStyle="ToolWindow"
        WindowStartupLocation="CenterOwner" >

    <Grid Background="WhiteSmoke">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <!--First row-->
        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Label Margin="5,15,5,5">Layer:</Label>
            <ComboBox x:Name="cbxLayer" Grid.Column ="1" Margin="5,15,10,5" HorizontalAlignment="Stretch" />
        </Grid>

        <!--Second row-->
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <Label Margin="5">Radius:</Label>
            <TextBox x:Name="txtRadius" Grid.Column="1" Margin="5" HorizontalAlignment="Stretch" TextChanged="txtRadius_TextChanged" />
            <Button Grid.Column="2" Margin="5,5,10,5" Content="    >    " Click="btnRadius_Click" />
        </Grid>

        <!--Third row-->
        <Grid Grid.Row="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Button x:Name="btnOK" Margin="10" HorizontalAlignment="Right" Content="OK" Height="24" Width="80" Click="btnOK_Click"/>
            <Button Grid.Column="1" Margin="10" HorizontalAlignment="Left" Content="Cancel" Height="24" Width="80" IsCancel="True" />
        </Grid>
    </Grid>
</Window>
            
ModalWpfDialog.xaml.cs
using Autodesk.AutoCAD.EditorInput;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

namespace AcadUISample.ModalWpf
{
    /// <summary>
    /// Interaction logic for ModalWpfDialog.xaml
    /// </summary>
    public partial class ModalWpfDialog : Window
    {
        // private field
        double radius;

        /// <summary>
        /// Gets the selected layer name.
        /// </summary>
        public string Layer => (string)cbxLayer.SelectedItem;

        /// <summary>
        /// Gets the radius.
        /// </summary>
        public double Radius => radius;

        /// <summary>
        /// Creates a new instance of ModalWpfDialog.
        /// </summary>
        /// <param name="layers">Layer names collection.</param>
        /// <param name="layer">Default layer name.</param>
        /// <param name="radius">Default radius</param>
        public ModalWpfDialog(List<string> layers, string layer, double radius)
        {
            InitializeComponent();
            this.radius = radius;
            cbxLayer.ItemsSource = layers;
            cbxLayer.SelectedItem = layer;
            txtRadius.Text = radius.ToString();
        }

        /// <summary>
        /// Handles the 'Click' event of the 'Radius' button.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event data.</param>
        private void btnRadius_Click(object sender, RoutedEventArgs e)
        {
            // prompt the user to specify distance
            var ed = AcAp.DocumentManager.MdiActiveDocument.Editor;
            var opts = new PromptDistanceOptions("\nSpecify the radius: ");
            opts.AllowNegative = false;
            opts.AllowZero = false;
            var pdr = ed.GetDistance(opts);
            if (pdr.Status == PromptStatus.OK)
            {
                txtRadius.Text = pdr.Value.ToString();
            }
        }

        /// <summary>
        /// Handles the 'Click' event of the 'OK' button.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event data.</param>
        private void btnOK_Click(object sender, RoutedEventArgs e)
        {
            DialogResult = true;
        }

        /// <summary>
        /// Handles the 'TextChanged' event ot the 'Radius' TextBox.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event data.</param>
        private void txtRadius_TextChanged(object sender, TextChangedEventArgs e)
        {
            btnOK.IsEnabled = double.TryParse(txtRadius.Text, out radius);
        }
    }
}
            

Back to the summary.




Modal dialog box (Binding)

In addition to rich content, WPF also offers a powerful data binding mechanism based in part on Dependency Properties, properties that can notify that their value has changed.

To use this mechanism, you must assign the data context (DataContext) to a class (here the one containing the code behind) and this one must implement the INotifyPropertyChanged interface.

To illustrate this, let's resume our modal dialog.
In XAML, some control dependency properties are linked to properties defined in the code behind. The class containing the code behind must therefore implement INotifyPropertyChanged.
Likewise, the 'Radius' and 'OK' buttons are linked to RoutedCommand instances defined in the code behind that manage their behaviors.
The use of WPF Binding allows you to avoid having to name the different controls.

To push things a little bit, let's add to the items in the drop-down list a square of the color of the layer.
In XAML, inside the ComboBox tag, we define an ItemTemplate to display the colored square next to the layer name. It is therefore necessary that the elements of the collection related to the ComboBox control have a property corresponding to the name of the layer and another of the type SolidColorBrush (the type used in WPF to draw a solid color).
We use here a dictionary containing the names of the layers (key) and the brush corresponding to the color of each layer (value).

Commands.cs
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

[assembly: CommandClass(typeof(AcadUISample.ModalWpfBinding.Commands))]

namespace AcadUISample.ModalWpfBinding
{
    public class Commands
    {
        // instance fields
        Document doc;
        Database db;
        Editor ed;
        double radius;      // radius default value
        string layerName;   // layer default value

        /// <summary>
        /// Crates a new instance of Commands.
        /// This constructor is run once per document
        /// at the first call of a 'CommandMethod' method.
        /// </summary>
        public Commands()
        {
            // private fields initialization (initial default values)
            doc = AcAp.DocumentManager.MdiActiveDocument;
            db = doc.Database;
            ed = doc.Editor;
            // initial default values
            layerName = (string)AcAp.GetSystemVariable("clayer");
            radius = 10.0;
        }

        /// <summary>
        /// Command to show the dialog box
        /// </summary>
        [CommandMethod("CMD_MODAL_WPF_BINDING")]
        public void ModalWpfDialogCmd()
        {
            var layers = GetLayerBrushes();
            if (!layers.ContainsKey(layerName))
            {
                layerName = (string)AcAp.GetSystemVariable("clayer");
            }
            var layer = layers.First(l => l.Key == layerName);

            // shows the dialog box dialogue
            var dialog = new ModalWpfDialog(layers, layer, radius);
            var result = AcAp.ShowModalWindow(dialog);
            if (result.Value)
            {
                // fields update
                layerName = dialog.Layer.Key;
                radius = dialog.Radius;

                // circle drawing
                var ppr = ed.GetPoint("\nSpecify the center: ");
                if (ppr.Status == PromptStatus.OK)
                {
                    // drawing the circle in current space
                    using (var tr = db.TransactionManager.StartTransaction())
                    {
                        var curSpace = 
                            (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
                        using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, radius))
                        {
                            circle.TransformBy(ed.CurrentUserCoordinateSystem);
                            circle.Layer = layerName;
                            curSpace.AppendEntity(circle);
                            tr.AddNewlyCreatedDBObject(circle, true);
                        }
                        tr.Commit();
                    }
                }
            }
        }

        /// <summary>
        /// Collects the layers and the brushes corresponding to their colors.
        /// </summary>
        /// <returns>Dictionary of the layers and brushes.</returns>
        private Dictionary<string, SolidColorBrush> GetLayerBrushes()
        {
            var layerBrushes = new Dictionary<string, SolidColorBrush>();
            using (var tr = db.TransactionManager.StartOpenCloseTransaction())
            {
                var layerTable = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);
                foreach (ObjectId id in layerTable)
                {
                    var layer = (LayerTableRecord)tr.GetObject(id, OpenMode.ForRead);
                    var drawingColor = layer.Color.ColorValue;
                    var mediaColor = Color.FromRgb(drawingColor.R, drawingColor.G, drawingColor.B);
                    layerBrushes.Add(layer.Name, new SolidColorBrush(mediaColor));
                }
            }
            return layerBrushes;
        }
    }
}
            
ModalWpfDialog.xaml
<Window x:Class="AcadUISample.ModalWpfBinding.ModalWpfDialog"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:AcadUISample.ModalWpfBinding"
        mc:Ignorable="d" 
        Title="ModalWpfDialog" 
        Height="160" Width="300" 
        MinHeight="160" MinWidth="250"
        WindowStyle="ToolWindow"
        WindowStartupLocation="CenterOwner" >

    <Window.CommandBindings>
        <CommandBinding Command="{x:Static local:ModalWpfDialog.OkCmd}"
                        Executed="OkCmdExecuted"
                        CanExecute="OkCmdCanExecute" />
        <CommandBinding Command="{x:Static local:ModalWpfDialog.RadiusCmd}"
                        Executed="RadiusCmdExecuted"
                        CanExecute="RadiusCanExecute" />
    </Window.CommandBindings>

    <Grid Background="WhiteSmoke">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <!--First row-->
        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Label Margin="5,15,5,5">Layer:</Label>
            <ComboBox Grid.Column ="1" Margin="5,15,10,5" HorizontalAlignment="Stretch" 
                      ItemsSource="{Binding Layers}" SelectedItem="{Binding Layer}">
                <!--Definition of a template for the drop down list-->
                <ComboBox.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>
                            <!--Square with the layer color-->
                            <Rectangle Grid.Column="0" Margin="3" VerticalAlignment="Stretch" 
                                       Width="{Binding Path=ActualHeight, 
                                               RelativeSource={RelativeSource Self}}"
                                       Stroke="Black" StrokeThickness="0.5" 
                                       Fill="{Binding Value}"/>
                            <!--Layer name-->
                            <TextBlock Grid.Column="1" VerticalAlignment="Center" 
                                       Text="{Binding Key}" />
                        </Grid>
                    </DataTemplate>
                </ComboBox.ItemTemplate>
            </ComboBox>
        </Grid>

        <!--Second row-->
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <Label Margin="5">Radius:</Label>
            <TextBox Grid.Column="1" Margin="5" HorizontalAlignment="Stretch" 
                     Text="{Binding TextRadius, UpdateSourceTrigger=PropertyChanged}" />
            <Button Grid.Column="2" Margin="5,5,10,5" Content="    >    " 
                    Command="{x:Static local:ModalWpfDialog.RadiusCmd}" />
        </Grid>

        <!--Third row-->
        <Grid Grid.Row="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Button Margin="10" HorizontalAlignment="Right" Content="OK" Height="24" Width="80" 
                    Command="{x:Static local:ModalWpfDialog.OkCmd}"/>
            <Button Grid.Column="1" Margin="10" HorizontalAlignment="Left" Content="Cancel" 
                    Height="24" Width="80" IsCancel="True" />
        </Grid>
    </Grid>
</Window>
            
ModalWpfDialog.xaml.cs
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;

using Autodesk.AutoCAD.EditorInput;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;


namespace AcadUISample.ModalWpfBinding
{
    /// <summary>
    /// Interaction logic for ModalWpfDialog.xaml.
    /// </summary>
    public partial class ModalWpfDialog : Window, INotifyPropertyChanged
    {
        #region INotifyPropertyChanged implementation

        /// <summary>
        /// Raised event when a property changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Method called in the setter of the properties whom changes have to be notified.
        /// </summary>
        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion

        // private fields
        KeyValuePair<string, SolidColorBrush> layer;
        double radius;
        string txtRad;
        bool validNumber;

        /// <summary>
        /// Gets the command bound to the 'OK' button.
        /// </summary>
        public static RoutedCommand OkCmd = new RoutedCommand();

        /// <summary>
        /// Gets the command bound to the 'Radius' button.
        /// </summary>
        public static RoutedCommand RadiusCmd = new RoutedCommand();

        /// <summary>
        /// Gets or sets the LayerData instance bounded to the selected layer in the comboBox control.
        /// </summary>
        public KeyValuePair<string, SolidColorBrush> Layer
        {
            get { return layer; }
            set { layer = value; OnPropertyChanged(nameof(Layer)); }
        }

        /// <summary>
        /// Gets the collection of layer data bound to the ComboBox control items.
        /// </summary>
        public Dictionary<string, SolidColorBrush> Layers { get; }

        /// <summary>
        /// Gets the radius.
        /// </summary>
        public double Radius => radius;

        /// <summary>
        /// Gets or sets the text in the 'Radius' edit box.
        /// </summary>
        public string TextRadius
        {
            get { return txtRad; }
            set
            {
                txtRad = value;
                ValidNumber = double.TryParse(value, out radius) && radius > 0.0;
                OnPropertyChanged(nameof(TextRadius));
            }
        }

        /// <summary>
        /// Gets or sets a value indicating
        /// Gets or sets a value indicating whether the text in the 'Radius'
        /// edit box represents a valid number.
        /// </summary>
        public bool ValidNumber
        {
            get { return validNumber; }
            set { validNumber = value; OnPropertyChanged(nameof(ValidNumber)); }
        }

        /// <summary>
        /// Creates a ew instance of ModalWpfDialog
        /// </summary>
        /// <param name="layers">Collection of layer data to be bound to the ComboBox control.</param>
        /// <param name="layer">Default layer data.</param>
        /// <param name="radius">Default radius.</param>
        public ModalWpfDialog(Dictionary<string, SolidColorBrush> layers, KeyValuePair<string, SolidColorBrush> layer, double radius)
        {
            InitializeComponent();
            // defines the data context
            DataContext = this;
            // initializes bindings
            Layers = layers;
            Layer = layer;
            TextRadius = radius.ToString();
        }

        /// <summary>
        /// Defines the action bound to 'OK' button.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event data.</param>
        private void OkCmdExecuted(object sender, ExecutedRoutedEventArgs e) =>
            DialogResult = true;

        /// <summary>
        /// Determines whether the linked action 'OK' button can be executed.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event data.</param>
        private void OkCmdCanExecute(object sender, CanExecuteRoutedEventArgs e) =>
            e.CanExecute = ValidNumber;

        /// <summary>
        /// Defines the action bound to 'Radius' button.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event data.</param>
        private void RadiusCmdExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            // prompt the user to specify a distance
            var ed = AcAp.DocumentManager.MdiActiveDocument.Editor;
            var opts = new PromptDistanceOptions("\nSpecify the radius: ");
            opts.AllowNegative = false;
            opts.AllowZero = false;
            var pdr = ed.GetDistance(opts);
            if (pdr.Status == PromptStatus.OK)
                TextRadius = pdr.Value.ToString();
        }

        /// <summary>
        /// Determines whether the linked action 'Radius' button can be executed.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event data.</param>>
        private void RadiusCanExecute(object sender, CanExecuteRoutedEventArgs e) =>
            e.CanExecute = true;
    }
}
            

back to the summary.




Palette with MVVM

The MVVM (Model View ViewModel) Design Pattern is particularly suited to WPF.
It allows to separate the "business" part, in general the data, (Model) from their presentation (View) and from the interaction logic between the two (ViewModel).
The "strict" application of this architecture may seem a little heavy (especially in the case of an example as simple as this one) but it encourages better structure of the code by optimizing the use of bindings.


The Model part does not contain any specific code here except for the circle drawing command. It can be considered as consisting of the AutoCAD API. For example, the Layer drop-down list will be linked to AutoCAD using the UIBindings API (a "dynamic" collection is needed for modeless mode).

Commands.cs
using Autodesk.AutoCAD.ApplicationServices.Core;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;

[assembly: CommandClass(typeof(AcadUISample.PaletteWpf.Commands))]

namespace AcadUISample.PaletteWpf
{
    public class Commands
    {
        // static field
        static CustomPaletteSet palette;

        // instance fields (default values)
        double radius = 10.0;
        string layer;

        /// <summary>
        /// Command to show the palette.
        /// </summary>
        [CommandMethod("CMD_PALETTE_WPF")]
        public void ShowPaletteSetWpf()
        {
            if (palette == null)
                palette = new CustomPaletteSet();
            palette.Visible = true;
        }

        /// <summary>
        /// Command to draw the circle.
        /// </summary>
        [CommandMethod("CMD_CIRCLE_WPF")]
        public void DrawCircleCmd()
        {
            var doc = Application.DocumentManager.MdiActiveDocument;
            var db = doc.Database;
            var ed = doc.Editor;

            // choose the layer
            if (string.IsNullOrEmpty(layer))
                layer = (string)Application.GetSystemVariable("clayer");
            var strOptions = new PromptStringOptions("\nLayer name: ");
            strOptions.DefaultValue = layer;
            strOptions.UseDefaultValue = true;
            var strResult = ed.GetString(strOptions);
            if (strResult.Status != PromptStatus.OK)
                return;
            layer = strResult.StringResult;

            // specify the radius
            var distOptions = new PromptDistanceOptions("\nSpecify the radius: ");
            distOptions.DefaultValue = radius;
            distOptions.UseDefaultValue = true;
            var distResult = ed.GetDistance(distOptions);
            if (distResult.Status != PromptStatus.OK)
                return;
            radius = distResult.Value;

            // specify the center
            var ppr = ed.GetPoint("\nSpecify the center: ");
            if (ppr.Status == PromptStatus.OK)
            {
                // draw the circle in current space
                using (var tr = db.TransactionManager.StartTransaction())
                {
                    var curSpace = 
                        (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
                    using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, distResult.Value))
                    {
                        circle.TransformBy(ed.CurrentUserCoordinateSystem);
                        circle.Layer = strResult.StringResult;
                        curSpace.AppendEntity(circle);
                        tr.AddNewlyCreatedDBObject(circle, true);
                    }
                    tr.Commit();
                }
            }
        }
    }
}
            


In the View section there is the description of the distributed user interface between:
- the CustomPaletteSet.cs file that describes the palette set;
- the PaletteTabView.xaml file that describes the user control and defines the bindings to ViewModel properties;
- the PaletteTabView.xaml.cs file, which is supposed to contain the Code Behind which is, in general, almost empty in the case of the MVVM architecture (the interaction logic belongs to the ViewModel part).

CustomPaletteSet.cs
using System;

using Autodesk.AutoCAD.ApplicationServices.Core;
using Autodesk.AutoCAD.Windows;

namespace AcadUISample.PaletteWpf
{
    internal class CustomPaletteSet : PaletteSet
    {
        // static field
        static bool wasVisible;

        /// <summary>
        /// Creates a new instance of CustomPaletteSet.
        /// </summary>
        public CustomPaletteSet()
            :base("Palette WPF", "CMD_PALETTE_WPF", new Guid("{42425FEE-B3FD-4776-8090-DB857E9F7A0E}"))
        {
            Style =
                PaletteSetStyles.ShowAutoHideButton |
                PaletteSetStyles.ShowCloseButton |
                PaletteSetStyles.ShowPropertiesMenu;
            MinimumSize = new System.Drawing.Size(250, 150);
            AddVisual("Circle", new PaletteTabView());

            // automatically hide the palette while none document is active (no document state)
            var docs = Application.DocumentManager;
            docs.DocumentBecameCurrent += (s, e) => 
                Visible = e.Document == null ? false : wasVisible;
            docs.DocumentCreated += (s, e) =>
                Visible = wasVisible;
            docs.DocumentToBeDeactivated += (s, e) => 
                wasVisible = Visible;
            docs.DocumentToBeDestroyed += (s, e) =>
            {
                wasVisible = Visible;
                if (docs.Count == 1)
                    Visible = false;
            };
        }
    }
}
      
PaletteTabView.xaml
<UserControl x:Class="AcadUISample.PaletteWpf.PaletteTabView"
             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:AcadUISample.PaletteWpf"
             mc:Ignorable="d" 
             d:DesignHeight="140" d:DesignWidth="250">
    <!--Definition of resources (color converter)-->
    <UserControl.Resources>
        <local:LayerColorConverter x:Key="colorConverter"/>
    </UserControl.Resources>
    
    <Grid Background="WhiteSmoke">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        
        <!--First row-->
        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Label Margin="5,15,5,5">Layer:</Label>
            <ComboBox x:Name="cbxLayer" Grid.Column ="1" Margin="5,15,10,5" HorizontalAlignment="Stretch" 
                      ItemsSource="{Binding Layers}" SelectedItem="{Binding Layer}">
                <!--Definition of a template for the drop down list-->
                <ComboBox.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>
                            <!--Square with the layer color-->
                            <Rectangle Grid.Column="0" Margin="3" VerticalAlignment="Stretch" 
                                       Width="{Binding Path=ActualHeight, RelativeSource={RelativeSource Self}}"
                                       Stroke="Black" StrokeThickness="0.5" 
                                       Fill="{Binding Color, Converter={StaticResource colorConverter}}"/>
                            <!--Layer name-->
                            <TextBlock Grid.Column="1" VerticalAlignment="Center" Text="{Binding Name}" />
                        </Grid>
                    </DataTemplate>
                </ComboBox.ItemTemplate>
            </ComboBox>
        </Grid>
        
        <!--Second row-->
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <Label Margin="5">Radius:</Label>
            <TextBox Grid.Column="1" Margin="5" HorizontalAlignment="Stretch" Text="{Binding TextRadius, UpdateSourceTrigger=PropertyChanged}" />
            <Button Grid.Column="2" Margin="5,5,10,5" Content="    >    " Command="{Binding GetRadiusCommand}"/>
        </Grid>
        
        <!--Third row-->
        <Button Grid.Row="2" Margin="5,15,5,5" Content="OK" Height="24" Width="80" Command="{Binding DrawCircleCommand}"/>
    </Grid>
</UserControl>
            
PaletteTabView.xaml.cs
using System.Windows.Controls;

namespace AcadUISample.PaletteWpf
{
    /// <summary>
    /// Interaction logic for PaletteTabView.xaml
    /// </summary>
    public partial class PaletteTabView : UserControl
    {
        public PaletteTabView()
        {
            InitializeComponent();
            // defines the data bindings with the ViewModel part
            DataContext = new PaletteTabViewModel();
        }
    }
}
            


The ViewModel part supports the interaction logic (what the code behind did in the previous example).
The PaletteTabViewModel class implements INotifyPropertyChanged through an inheritance relationship to handle properties as in the previous example.
For the buttons, we use the Command property instead of the 'Click' event and its handler in the code behind, which makes it possible to separate the presentation (View) from its logic (ViewModel). This property must be of a type that implements the ICommand interface.

We thus find in this part two small classes quite unavoidable in the MVVM architecture:
- ObservableObject that implements the INotifyPropertyChanged interface
- RelayCommand that implements the ICommand interface

In the ViewModel part, there are also some resources needed for the presentation, with a LayerColorConverter class to convert an element of type Autodesk.AutoCAD.Colors.Color representing the color of a layer (elements of the drop-down list) into an instance SolidColorBrush of the corresponding color; the filling of the square will be linked to this Converter.

PaletteTabViewModel.cs
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Windows.Data;
using System.ComponentModel;
using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

namespace AcadUISample.PaletteWpf
{
    class PaletteTabViewModel : ObservableObject
    {
        // private fields
        ICustomTypeDescriptor layer;
        double radius;
        string txtRad;
        bool validRad;

        /// <summary>
        /// Gets the Command object bound to the OK button.
        /// The button automatically disabled if CanExecute predicate returns false.
        /// </summary>
        public RelayCommand DrawCircleCommand =>
            new RelayCommand((_) => DrawCircle(), (_) => validRad);

        /// <summary>
        /// Gets the Command object bound to the Radius button (>).
        /// </summary>
        public RelayCommand GetRadiusCommand =>
            new RelayCommand((_) => GetRadius(), (_) => true);

        /// <summary>
        /// Gets or sets the selected layer.
        /// </summary>
        public ICustomTypeDescriptor Layer
        {
            get { return layer; }
            set { layer = value; OnPropertyChanged(nameof(Layer)); }
        }

        /// <summary>
        /// Gets the layer collection.
        /// </summary>
        public DataItemCollection Layers => AcAp.UIBindings.Collections.Layers;

        /// <summary>
        /// Gets or sets the radius value displayed in the Textbox.
        /// </summary>
        public string TextRadius
        {
            get { return txtRad; }
            set
            {
                txtRad = value;
                validRad = double.TryParse(value, out radius) && radius > 0.0;
                OnPropertyChanged(nameof(TextRadius));
            }
        }

        /// <summary>
        /// Creates a new instance of PaletteTabViewModel.
        /// </summary>
        public PaletteTabViewModel()
        {
            TextRadius = "10";
            Layer = Layers.CurrentItem;
            Layers.CollectionChanged += (s, e) => Layer = Layers.CurrentItem;
        }

        /// <summary>
        /// Method called by DrawCircleCommand. 
        /// Launches the CMD_CIRCLE_WPF command with current options.
        /// </summary>
        private void DrawCircle() =>
            AcAp.DocumentManager.MdiActiveDocument?.SendStringToExecute(
                $"CMD_CIRCLE_WPF \"{((INamedValue)Layer).Name}\" {TextRadius} ", false, false, false);

        /// <summary>
        /// Method called by GetRadiusCommand.
        /// </summary>
        private void GetRadius()
        {
            // prompt the user to specify a distance
            var ed = AcAp.DocumentManager.MdiActiveDocument.Editor;
            var opts = new PromptDistanceOptions("\nSpecify the radius: ");
            opts.AllowNegative = false;
            opts.AllowZero = false;
            var pdr = ed.GetDistance(opts);
            if (pdr.Status == PromptStatus.OK)
                TextRadius = pdr.Value.ToString();
        }
    }
}
            
ObservableObject.cs
using System.ComponentModel;

namespace AcadUISample.PaletteWpf
{
    /// <summary>
    /// Provides a type implementing INotifyPropertyChanged
    /// </summary>
    class ObservableObject : INotifyPropertyChanged
    {
        /// <summary>
        /// Raised event when a property changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Method called in the 'setter' of propertie whom changes have to be notified.
        /// </summary>
        /// <param name="propertyName">Property name.</param>
        protected void OnPropertyChanged(string propertyName) =>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
            
RelayCommand.cs
using System;
using System.Windows.Input;

namespace AcadUISample.PaletteWpf
{
    /// <summary>
    /// Provides a type implementing ICommand
    /// </summary>
    class RelayCommand : ICommand
    {
        readonly Action<object> execute;
        readonly Predicate<object> canExecute;

        /// <summary>
        /// Creates a new instance of RelayCommand.
        /// </summary>
        /// <param name="execute">Action to execute.</param>
        /// <param name="canExecute">Predicate indicating if the action can be executed.</param>
        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            this.execute = execute;
            this.canExecute = canExecute;
        }

        /// <summary>
        /// Executes the action passed as parameter to the constructor.
        /// </summary>
        /// <param name="parameter">Action parameter (may be null).</param>
        public void Execute(object parameter) => execute(parameter);

        /// <summary>
        /// Executes the predicate passed as parameter to the constructor.
        /// </summary>
        /// <param name="parameter">Predicate parameter (may be null).</param>
        /// <returns>Result of the predictae execution.</returns>
        public bool CanExecute(object parameter) => canExecute(parameter);

        /// <summary>
        /// Event indicating that the returned value of the predicte changed.
        /// </summary>
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
    }
}
            
LayerColorConverter.cs
using System;
using System.ComponentModel;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;

namespace AcadUISample.PaletteWpf
{
    /// <summary>
    /// Provides the conversion method to get a layer color.
    /// </summary>
    [ValueConversion(typeof(ICustomTypeDescriptor), typeof(SolidColorBrush))]
    class LayerColorConverter : IValueConverter
    {
        /// <summary>
        /// Converts an ICustomTypeDescriptor object figuring a layer into its color.
        /// </summary>
        /// <param name="value">AutoCAD color to convert.</param>
        /// <param name="targetType">SolidColorBrush type.</param>
        /// <param name="parameter">Unused.</param>
        /// <param name="culture">Unused.</param>
        /// <returns>Instance of SolidColorBrush figuring the layer color.</returns>
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value != null && value is Autodesk.AutoCAD.Colors.Color)
            {
                var acadColor = (Autodesk.AutoCAD.Colors.Color)value;
                var drawingColor = acadColor.ColorValue;
                var mediaColor = Color.FromRgb(drawingColor.R, drawingColor.G, drawingColor.B);
                return new SolidColorBrush(mediaColor);
            }
            return null;
        }

        /// <summary>
        /// Inverse converion method (unused).
        /// </summary>
        /// <returns>Always null</returns>
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return null;
        }
    }
}
            

Back to the summary.


Gilles Chanteau