October 2014

Sun Mon Tue Wed Thu Fri Sat
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  










« Changing the colour of nested AutoCAD entities through .NET | Main | Using .NET reflection with AutoCAD to change object properties - Part 2 »

February 05, 2007

Using .NET reflection with AutoCAD to change object properties - Part 1

I ended up having a fun weekend, in spite of the circumstances. My wife and eldest son were both sick with a cold (which, inevitably enough, I now feel I’m coming down with myself), so we mostly stayed around at home. So I got to spend a little time working on an idea that came to me after my last post.

While the code I provided last time does pretty much exactly what was asked of it (allowing the person who requested it to change the colour of every entity in the modelspace – whether in nested blocks or not – for them to be coloured “by block”), it occurred to me that to be a really useful tool we should go one step further and enable two things:

a) Allow the user to specify the kind of object to apply the change to
b) Allow the user to select the specific property that should be changed

What we'd actually end up with, doing this, is a CHPROP on steroids - a command-line interface to provide deep object property modification (down through nested blocks), on any kind of object (as long as it - or one of its ancestor classes - provides a .NET interface). This is cool functionality for people needing to go and massage data, though clearly is potentially quite a scary tool in the wrong hands (thank goodness for the undo mechanism!).

The specific programming problem we need to solve comes down to runtime querying/execution of code and is quite interesting: one that’s easy to solve in LISP (thanks to the combination of the (read) and (eval) functions) and in COM (using type information to call through to IDispatch-declared functions exposed via the v-table), but is almost impossible in C++. With ObjectARX you can use class factories to create instances of objects that were not compiled in (we do this when we load a drawing – we find out the class name of each object being read in, call its static class factory method available in the class’ corresponding AcRxClass object, and pass the information we read in to the dwgInFields() function to resurrect a valid instance of the object). But it’s much harder to query at runtime for a particular method to be called – you could hardcode it for the built-in protocol, but any new objects would cause a problem.

But anyway – all this to say that the way to do it in .NET is to use our old friend Reflection (I love it when a couple of recent topics converge like this, although I wish I could say it was all part of some grand plan… :-)

So, there are four things we actually need to use reflection for in this sample:

1) Get a System.Type from a string:

System.Type objType = System.Type.GetType(typeName);

2) Using the Type object get a PropertyInfo from a string:

System.Reflection.PropertyInfo propInfo = objType.GetProperty(propName);

3) Check whether an entity is of a particular type:

res = objType.IsInstanceOfType(ent);

4) Access the property on a particular entity:

Get...

val = objType.InvokeMember(propName, BindingFlags.GetProperty, null, ent, args);

Set...

objType.InvokeMember(propName, BindingFlags.SetProperty, null, ent, args);

That's basically all there is to it, but I'm going to drag this out over a couple of posts, as the code does get quite involved.

This first post is going to focus on the user-input aspect of the code - querying the user for the various bits of information we need, and getting the type & property information using reflection. So it really only looks at the first two items on the above list.

Here's the C# code itself - the main user-input function is called  SelectClassPropertyAndValue(), and I've defined a simple TEST command to simply call it and return the results.

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Colors;

using System.Reflection;


namespace PropertyChanger

{

  public class PropertyChangerCmds

  {

    [CommandMethod("TEST")]

    public void TestInput()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;


      System.Type objType;

      string propName;

      object newPropValue;

      bool recurse;


      if (SelectClassPropertyAndValue(

            out objType,

            out propName,

            out newPropValue,

            out recurse

          )

        )

      {

        ed.WriteMessage(

          "\nType selected: " + objType.Name +

          "\nProperty selected: " + propName +

          "\nValue selected: " + newPropValue +

          "\nRecurse chosen: " + recurse

        );

      }

      else

      {

        ed.WriteMessage(

          "\nFunction returned false."

        );

      }

    }


    private bool SelectClassPropertyAndValue(

      out System.Type objType,

      out string propName,

      out object newPropValue,

      out bool recurse)

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;


      objType = null;

      propName = "";

      newPropValue = null;

      recurse = true;


      // Let's first get the class to query for

      PromptResult ps =

        ed.GetString(

          "\nEnter type of objects to look for: "

        );


      if (ps.Status == PromptStatus.OK)

      {

        string typeName = ps.StringResult;


        // Use reflection to get the type from the string

        objType =

          System.Type.GetType(

            typeName,

            false,        // Do not throw an exception

            true          // Case-insensitive search

          );


        // If we didn't find it, try prefixing with

        // "Autodesk.AutoCAD.DatabaseServices."


        if (objType == null)

        {

          objType =

            System.Type.GetType(

              "Autodesk.AutoCAD.DatabaseServices." +

              typeName + ", acdbmgd",

              false,      // Do not throw an exception

              true        // Case-insensitive search

            );

        }


        if (objType == null)

        {

          ed.WriteMessage(

            "\nType " + typeName + " not found."

          );

        }

        else

        {

          // If we have a valid type then let's

          // first list its writable properties

          ListProperties(objType);


          // Prompt for a property

          ps = ed.GetString(

            "\nEnter property to modify: "

          );


          if (ps.Status == PromptStatus.OK)

          {

            propName = ps.StringResult;


            // Make sure the property exists...

            System.Reflection.PropertyInfo propInfo =

              objType.GetProperty(propName);

            if (propInfo == null)

            {

              ed.WriteMessage(

                "\nProperty " +

                propName +

                " for type " +

                typeName +

                " not found."

              );

            }

            else

            {

              if (!propInfo.CanWrite)

              {

                ed.WriteMessage(

                  "\nProperty " +

                  propName +

                  " of type " +

                  typeName +

                  " is not writable."

                );

              }

              else

              {

                // If the property is writable...

                // ask for the new value

                System.Type propType = propInfo.PropertyType;

                string prompt =

                      "\nEnter new value of " +

                      propName +

                      " property for all objects of type " +

                      typeName +

                      ": ";


                // Only certain property types are currently

                // supported: Int32, Double, String, Boolean

                switch (propType.ToString())

                {

                  case "System.Int32":

                    PromptIntegerResult pir =

                      ed.GetInteger(prompt);

                    if (pir.Status == PromptStatus.OK)

                      newPropValue = pir.Value;

                    break;

                  case "System.Double":

                    PromptDoubleResult pdr =

                      ed.GetDouble(prompt);

                    if (pdr.Status == PromptStatus.OK)

                      newPropValue = pdr.Value;

                    break;

                  case "System.String":

                    PromptResult psr =

                      ed.GetString(prompt);

                    if (psr.Status == PromptStatus.OK)

                      newPropValue = psr.StringResult;

                    break;

                  case "System.Boolean":

                    PromptKeywordOptions pko =

                      new PromptKeywordOptions(

                      prompt);

                    pko.Keywords.Add("True");

                    pko.Keywords.Add("False");

                    PromptResult pkr =

                      ed.GetKeywords(pko);

                    if (pkr.Status == PromptStatus.OK)

                    {

                      if (pkr.StringResult == "True")

                        newPropValue = true;

                      else

                        newPropValue = false;

                    }

                    break;

                  default:

                    ed.WriteMessage(

                      "\nProperties of type " +

                      propType.ToString() +

                      " are not currently supported."

                    );

                    break;

                }

                if (newPropValue != null)

                {

                  PromptKeywordOptions pko =

                    new PromptKeywordOptions(

                      "\nChange properties in nested blocks: "

                    );

                  pko.AllowNone = true;

                  pko.Keywords.Add("Yes");

                  pko.Keywords.Add("No");

                  pko.Keywords.Default = "Yes";

                  PromptResult pkr =

                    ed.GetKeywords(pko);

                  if (pkr.Status == PromptStatus.None |

                      pkr.Status == PromptStatus.OK)

                  {

                    if (pkr.Status == PromptStatus.None |

                        pkr.StringResult == "Yes")

                      recurse = true;

                    else

                      recurse = false;


                    return true;

                  }

                }

              }

            }

          }

        }

      }

      return false;

    }


    private void ListProperties(System.Type objType)

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;


      ed.WriteMessage(

        "\nWritable properties for " +

        objType.Name +

        ": "

      );


      PropertyInfo[] propInfos =

        objType.GetProperties();

      foreach (PropertyInfo propInfo in propInfos)

      {

        if (propInfo.CanWrite)

        {

          ed.WriteMessage(

            "\n  " +

            propInfo.Name +

            " : " +

            propInfo.PropertyType

          );

        }

      }

      ed.WriteMessage("\n");

    }

  }

}

There are a few points I should make about this code:

  • GetType() needs an assembly qualified name. The above code makes two calls to this function, one without the "Autodesk.AutoCAD.DatabaseServices." prefix and the ", acdbmgd" suffix, in case we want to get another type of class, but if that fails then the prefix/suffix get added.
  • To make it easier for the user to select a writable property, I've implemented a separate ListProperties() function that iterates through the available properties and provides the name and type for each one that's writable.
  • Only properties of these datatypes are currently supported: System.Int32, System.Double, System.String, System.Boolean. It should be simple enough to support other datatypes (Vector3d, Point3d etc.), if you have the time and the inclination.

That's about it for the UI portion of the code... let's take a look at what happens when this code runs:

Command: TEST


Enter type of objects to look for: Circle


Writable properties for Circle:

  Normal : Autodesk.AutoCAD.Geometry.Vector3d

  Thickness : System.Double

  Radius : System.Double

  Center : Autodesk.AutoCAD.Geometry.Point3d

  EndPoint : Autodesk.AutoCAD.Geometry.Point3d

  StartPoint : Autodesk.AutoCAD.Geometry.Point3d

  MaterialMapper : Autodesk.AutoCAD.GraphicsInterface.Mapper

  MaterialId : Autodesk.AutoCAD.DatabaseServices.ObjectId

  Material : System.String

  ReceiveShadows : System.Boolean

  CastShadows : System.Boolean

  LineWeight : Autodesk.AutoCAD.DatabaseServices.LineWeight

  Visible : System.Boolean

  LinetypeScale : System.Double

  LinetypeId : Autodesk.AutoCAD.DatabaseServices.ObjectId

  Linetype : System.String

  LayerId : Autodesk.AutoCAD.DatabaseServices.ObjectId

  Layer : System.String

  PlotStyleNameId : Autodesk.AutoCAD.DatabaseServices.PlotStyleDescriptor

  PlotStyleName : System.String

  Transparency : Autodesk.AutoCAD.Colors.Transparency

  ColorIndex : System.Int32

  Color : Autodesk.AutoCAD.Colors.Color

  HasSaveVersionOverride : System.Boolean

  XData : Autodesk.AutoCAD.DatabaseServices.ResultBuffer

  MergeStyle : Autodesk.AutoCAD.DatabaseServices.DuplicateRecordCloning

  OwnerId : Autodesk.AutoCAD.DatabaseServices.ObjectId

  AutoDelete : System.Boolean


Enter property to modify: Radius


Enter new value of Radius property for all objects of type Circle: 3.14159


Change properties in nested blocks [Yes/No] <Yes>: Y


Type selected: Circle

Property selected: Radius

Value selected: 3.14159

Recurse chosen: True

In the next post we'll hook this up with some code to actually call the properties on various objects, recursing through block definitions, as needed.

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00d83452464869e200d834e0e48353ef

Listed below are links to weblogs that reference Using .NET reflection with AutoCAD to change object properties - Part 1:

blog comments powered by Disqus

Feed/Share

10 Random Posts