Creating demand-loading entries automatically for your AutoCAD application using C#, F# or VB.NET

This week I'm going to posting a few topics related to F#, as it feels as though I've been neglecting it, of late. And as this technology is going to hit the mainstream very soon – when Visual Studio 2010 ships – it seems all the more important to keep one's F# skills honed.

We're going to start the week with an F# equivalent to the code shown in this previous post, where we go through and reflect on the commands exposed by an assembly in order to create corresponding demand-loading Registry keys automatically.

We've shipped VB.NET and C# versions of this code as part of our Plugins of the Month, but I wanted to make sure the latest versions were posted here, all in one place:

I've made a few general changes to the previous approach:

  • More use of "using" to make sure that Registry keys get closed properly
    • Not that this caused any particular issues of which I'm aware, but it's certainly cleaner
  • Use of CreateSubKey() to open the Applications key for write, or to create it if it doesn't exist
    • This can apparently be a problem on some vertical versions of AutoCAD such as AutoCAD Electrical

Let's take a look at the F# code, in particular:

module DemandLoading.RegistryUpdate

 

open System.Reflection

open System.Resources

open Microsoft.Win32

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.Runtime

 

// Get the information from a custom attribute, if of the right type

 

let commandInfo (rm : ResourceManager) (attb : obj) =

  let cma = attb :?> CommandMethodAttribute

  if cma <> null then

 

    // Harvest the information about each command

 

    match cma.LocalizedNameId with

    | null -> (cma.GlobalName, cma.GlobalName, cma.GroupName)

    | _ ->

      try

        (cma.GlobalName,

        rm.GetString(cma.LocalizedNameId),

        cma.GroupName)

      with _ ->

        (cma.GlobalName, cma.GlobalName, cma.GroupName)

  else

    (null, null, null)

 

let commandsFromMethod rm (meth : MethodInfo) =

  Array.map (commandInfo rm)

    (meth.GetCustomAttributes (typeof<CommandMethodAttribute>, true))

 

let commandsFromType assem (t : System.Type) =

  let rm = new ResourceManager(t.FullName, assem)

  rm.IgnoreCase <- true

  Array.map (commandsFromMethod rm) (t.GetMethods()) |> Array.concat

 

let commandsFromModule assem (m : Module) =

  Array.map (commandsFromType assem) (m.GetTypes()) |> Array.concat

 

let commandsFromAssembly assem =

  Array.map (commandsFromModule assem) (assem.GetModules(true))

    |> Array.concat

 

let createDemandLoadingEntries name path currentUser cmds =

 

  // Choose the Registry hive according to the inputs

 

  let hive =

    if currentUser then

      Registry.CurrentUser

    else

      Registry.LocalMachine

 

  // Check whether any valid commands exist (the command-names are

  // the first two entries in the tuple contained in the command

  // information array)

 

  let hasCmds =

    Array.exists (fun (a,b,c) -> a <> null && b <> null) cmds

 

  // And the same for groups, which is the third entry

 

  let hasGrps = Array.exists (fun (a,b,c) -> c <> null) cmds

 

  // Define whether to load the module on startup (if no commands)

  // or on command invocation (if any are defined)

 

  let flags = if hasCmds then 12 else 2

 

  // Open the main AutoCAD (or vertical) and "Applications" keys

 

  use ack =

    hive.OpenSubKey

      (HostApplicationServices.Current.RegistryProductRootKey,

      true)

  use appk = ack.CreateSubKey("Applications")

 

  // Already registered? Just return

 

  if not

    (Array.exists (fun x -> x = name) (appk.GetSubKeyNames())) then

 

    // Create the our application's root key and its values

 

    use rk = appk.CreateSubKey(name)

    rk.SetValue("DESCRIPTION", name, RegistryValueKind.String)

    rk.SetValue("LOADCTRLS", flags, RegistryValueKind.DWord)

    rk.SetValue("LOADER", path, RegistryValueKind.String)

    rk.SetValue("MANAGED", 1, RegistryValueKind.DWord)

 

    // Create a subkey if there are any commands...

 

    if hasCmds then

      use ck = rk.CreateSubKey("Commands")

      let createCommand (key : RegistryKey) info =

        match info with

        | (null, _, _) -> () // Ignore any null global commands

        | (_, null, _) -> () // Ignore any null local commands

        | (glob, loc, _) ->

          key.SetValue(glob,loc,RegistryValueKind.String)

 

      Array.iter (createCommand ck) cmds |> ignore

 

    // And the command groups, if there are any

 

    if hasGrps then

      use gk = rk.CreateSubKey("Groups")

      let createGroup (key : RegistryKey) info =

        match info with

        | (_, _, null) -> ()

        | (_, _, group) ->

          key.SetValue(group, group,RegistryValueKind.String)

 

      Array.iter (createGroup gk) cmds

 

let removeDemandLoadingEntries name currentUser =

 

  // Choose the Registry hive according to the input

 

  let hive =

    if currentUser then

      Registry.CurrentUser

    else

      Registry.LocalMachine

 

  // Open the main AutoCAD (or vertical) and "Applications" keys

 

  use ack =

    hive.OpenSubKey

      (HostApplicationServices.Current.RegistryProductRootKey)

  use appk = ack.OpenSubKey("Applications", true)

 

  // Delete the key with the same name as this assembly

 

  appk.DeleteSubKeyTree(name)

 

let RegisterForDemandLoading() =

 

  // Get the current assembly

 

  let assem = Assembly.GetExecutingAssembly()

 

  // Get the command information and create Registry

  // entries from it

 

  commandsFromAssembly assem

    |> createDemandLoadingEntries

        (assem.GetName().Name) assem.Location true

 

let UnregisterForDemandLoading() =

  removeDemandLoadingEntries

    (Assembly.GetExecutingAssembly().GetName().Name) true

I've tried to structure the F# code somewhat differently to the prior, more imperative versions of the code: it has more emphasis on the application of functions and the flow of data between them. It also makes use of higher order functions such as Array.map and Array.iter to apply functions to the contents of data structures (in this case arrays), and uses pattern-matching where sequences of if-then-else statements would have proven cumbersome.

Later in the week we'll see this added to the Spirograph application, to create its demand-loading information automatically on startup, but first we're going to need to see how to implement the IExtensionApplication interface from an F# application…

  1. Hi,Kean
    It seems that we cannot write AutoCAD programs with Visual Studio 2010?

  2. Hi 才鸟,

    That's not my understanding. While I haven't actually done this, myself, I'm sure it can be done (the architect of our .NET interface has been testing with VS 2010 from the beginning).

    Regards,

    Kean

  3. It seems AutoCAD is not compatible with .NET 4.0 (My tests with the latest RC), While you can use VS2010, you will need to target the older CLR. I hope this changes with the RTM

  4. Great point - had forgotten about that aspect of it.

    Albert presented a session on .NET 4.0 at AU 2009, which includes information on getting AutoCAD to use that version of the framework (and the fact there's an incompatibility). See Albert's presentation for details.

    Kean

  5. Kean

    Probably me doing something wrong, but the demand loading (VB.net) code worked great on ACA 2010 but doesn't seem to work on ACA 2011. No error messages just doesn't seem to do anything 🙁

    Has anyone else got this working on 2011?

  6. Hugh,

    I haven't tried it in ACA 2011, but in AutoCAD 2011 it appears to work fine.

    Could you download and NETLOAD the Clipboard Manager from Labs (in the plugin catalog) and see if that creates Registry entries on your system?

    Cheers,

    Kean

  7. Thanks!! Just what I was looking for. I'm using VS 2010, but FW 3.5, and works fine.

  8. Thanks!! Just what I was looking for. I'm using VS 2010, but FW 3.5, and works fine.

  9. Howdy!
    Around line 94, HostApplicationServices.Current.RegistryProductRootKey is not a member. Apparently these would be valid?

    Autodesk.AutoCAD.DatabaseServices.HostApplicationServices.MachineRegistryProductRootKey
    Autodesk.AutoCAD.DatabaseServices.HostApplicationServices.UserRegistryProductRootKey
    Could you please tell me how to fix this?

    1. It's possible there's been a change to the API that renders the code invalid... please post to the AutoCAD .NET forum if you need help confirming what needs changing.

      Kean

      1. jessicaenglishbartram Avatar
        jessicaenglishbartram

        keanw already

Leave a Reply to Sebas Cancel reply

Your email address will not be published. Required fields are marked *