What's new

Adventures in V24 Add-on Development

bolsover

Senior Member
I've lately been experimenting with writing an Add On for Alibre V24.
I thought it might be helpful to share some of my experience.
The first thing I should note is that what I have been working on is just for my own interest - but very happy to share with others.

AlibreX documentation
There is quite a lot missing from the AlibreX documentation with respect to Add On development and the absence of worked examples makes life quite difficult!

My starting point was to look over the AlibreX documentation. The first challenge I faced was that the Add-On introduction only references C++ code for the all important AlibreAddOn class.
I wanted to write code using C#. I'm not particularly experienced with C# - but it is a lot closer to Java - where I have 20+ years experience developing desktop applications for manufacturing.

Decompile AlibreX and AlibreAddOn
Lucky for me, I have a licence for Jetbrains DotPeek whicth includes a decompiler allowing me to peek into the Alibre Code (which appears to be written in C#) to see how things are done internally.
Trawling through the Alibre code, I discovered that it is possible to write an Add-On using C# but that there are a couple of prerequisites.
The first thing I found is that the AlibreDesignAddOn .adc file requires an undocumented change.

Original Alibre .adc:
XML:
<AlibreDesignAddOn specificationVersion="1" friendlyName="My AddOn">
   <Author name="MyName" link="http://URL"/>
   <DLL loadedWhen="Startup" location="MyAddOnName.dll"/>
   <Copyright> My CopyRight Information</Copyright>
   <Icon location="MyAddOn.ico"/>
   <Menu text="My AddOn"/>
   <Description> XYZ </Description>
   <Workspace type="Part"/>
   <Property name="Identifier" value="{DA8322C2-CE32-450C-2E43-5CG77C52D4B4}"/>
</AlibreDesignAddOn>

The above needs a modification:
Note <DLL type="Managed" and specificationVersion="2"
XML:
<AlibreDesignAddOn specificationVersion="2" friendlyName="My AddOn">
   <Author name="MyName" link="http://URL"/>
   <DLL type="Managed" loadedWhen="Startup" location="MyAddOnName.dll"/>
   <Copyright> My CopyRight Information</Copyright>
   <Icon location="MyAddOn.ico"/>
   <Menu text="My AddOn"/>
   <Description> XYZ </Description>
   <Workspace type="Part"/>
   <Property name="Identifier" value="{DA8322C2-CE32-450C-2E43-5CG77C52D4B4}"/>
</AlibreDesignAddOn>

The type="Managed" appears to instruct Alibre to load a C# .NET compatible Add-On - without this flag my code simply would not work.
I'm not too sure what specificationVersion="2" does.

The AlibreAddOn class
The next important job is to create the AlibreAddOn class with implementations of the three methods discussed in the Alibre documentation:
C#:
using System;
using AlibreAddOn;
using AlibreX;
using Bolsover;


namespace AlibreAddOnAssembly
{
    public static class AlibreAddOn
    {
        private static IADRoot alibreRoot;
        private static IntPtr parentWinHandle;
        private static DataBrowserForAlibreImpl AlibreAddOnInterface;

        public static void AddOnLoad(IntPtr hwnd, IAutomationHook pAutomationHook, IntPtr unused)
        {
            alibreRoot = (IADRoot) pAutomationHook.Root;
            parentWinHandle = hwnd;
            AlibreAddOnInterface = new DataBrowserForAlibreImpl(alibreRoot, parentWinHandle);
        }


        public static void AddOnInvoke(
            IntPtr hwnd,
            IntPtr pAutomationHook,
            string sessionName,
            bool isLicensed,
            int reserved1,
            int reserved2)
        {
        }


        public static void AddOnUnload(
            IntPtr hwnd,
            bool forceUnload,
            ref bool cancel,
            int reserved1,
            int reserved2)
        {
          
        }


        public static IAlibreAddOn GetAddOnInterface()
        {
            return (IAlibreAddOn) AlibreAddOnInterface;
        }
    }
}

Two important things to note about the above: the namespace and the class name - BOTH need to match the above for the code to be loaded by Alibre - ask me how I know!!
I've only actually added any real custom code to the AddOnLoad method. This loads a custom IAlibreAddOn where all the heavy lifting for an AddOn application starts - I'll dig a little deeper into that file in another post.

db
 

bolsover

Senior Member
Making progress..
I've added a whole bunch of code to this project and made available on GitHub:
https://github.com/bolsover/UtilitiesForAlibre

Utilities Menu.jpg

AlibreAddOn class
I've made a couple of small edits to the code in previous post - mainly to differentiate code in this project from the standalone DataBrowser I worked up to allow viewing and editing of Alibre Design Properties. Code for that project is also available on GitHub. https://github.com/bolsover/DataBrowserForAlibre.

The updated AlibreAddOn references a static class UtilitiesForAlibre which implements the Alibre IAlibreAddOn interface (defined in the AlibreAddOn package).
I won't post the whole code for the class UtilitiesForAlibre here - please download the project if you are interested.
A few notes about UtilitiesForAlibre and the interface IAlibreAddOn are however appropriate since this is key to making an add on work.

The following code is the essence of the interface IAlibreAddOn.

C#:
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;

namespace AlibreAddOn
{
  public interface IAlibreAddOn
  {
     //Description("Returns the menu ID of the add-on's root menu item")]
    int RootMenuItem {  get; }

    //Description("Returns Whether the given Menu ID has any sub menus")]
    bool HasSubMenus(int menuID);

    /Description("Returns the ID's of sub menu items under a popup menu item; the menu ID of a 'leaf' menu becomes its command ID")]
    Array SubMenuItems(int menuID);

    //Description("Returns the display name of a menu item; a menu item with text of a single dash (“-“) is a separator")]
    string MenuItemText(int menuID);

    //Description("Returns True if input menu item has sub menus")]
    bool PopupMenu(int menuID);

    //Description("Returns property bits providing information about the state of a menu item")]
    ADDONMenuStates MenuItemState(int menuID, string sessionIdentifier);

    //Description("Returns a tool tip string if input menu ID is that of a 'leaf' menu item")]
    string MenuItemToolTip(int menuID);

    //Description("Returns True if AddOn has updated Persistent Data")]
    bool HasPersistentDataToSave(string sessionIdentifier);

    //Description("Invokes the add-on command identified by menu ID; returning the add-on command interface is optional ")]
    IAlibreAddOnCommand InvokeCommand( int menuID,  string sessionIdentifier);

    //Description("Loads Data from AddOn")]
    void LoadData( IStream pCustomData, string sessionIdentifier);

   //Description("Saves Data to AddOn")]
    void SaveData(IStream pCustomData,  string sessionIdentifier);

   //Description("Sets the IsLicensed bit for the tightly coupled Add-on")]
    void setIsAddOnLicensed( bool isLicensed);

    //Description("Returns the icon name (with extension) for a menu item; the icon will be searched under the folder where the add-on's .adc file is present")]
    string MenuIcon( int menuID);

    //Description("Returns True if the AddOn needs to use a Dedicated Ribbon Tab")]
    bool UseDedicatedRibbonTab();
  }
}

How to use the interface interface IAlibreAddOn

Understanding this interface is key to enabling a custom AddOn. It is the wrapper that contains all the menu items available in the AddOn. You can of course reference my implementation UtilitiesForAlibre but a few notes are also appropriate..

First the RootMenuItem. This is an int. I had no clue as to what int to apply 0, 1 or 50000000. I made a guess that it didn't really matter and chose 401. I'm not sure if this was a lucky guess or if it really doesn't matter - but it worked.

Since I wanted a bunch of menu and sub-menu items, I actually defined a few constant int values:

C#:
        private const int MENU_ID_ROOT = 401;
        private const int MENU_ID_FILE = 501;
        private const int SUBMENU_IDS_FILE_OPEN = 502;
        private const int SUBMENU_IDS_FILE_CLOSE = 503;
        private const int SUBMENU_IDS_FILE_EXIT = 504;
        private const int SUBMENU_IDS_DATA_BROWSER = 505;
        private const int MENU_ID_UTILS = 601;
        private const int SUBMENU_IDS_UTILS_CYCLOIDAL_GEAR = 602;

Using constants like this turned out to be a good decision since the method "Array SubMenuItems(int menuID);" expects an array of int for each sub-menu.
This led me to define a few more private properties which get initialized in the constructor:

C#:
private readonly int[] MENU_IDS_FILE;
        private readonly int[] MENU_IDS_UTILS;
        private readonly int[] MENU_IDS_ROOT;

        private IADRoot alibreRoot;
        //  private IntPtr parentWinHandle; // no use for this at present but could be passes in via contuctor

        public UtilitiesForAlibre(IADRoot alibreRoot)
        {
            this.alibreRoot = alibreRoot;
            //  this.parentWinHandle = parentWinHandle;
            MENU_IDS_FILE = new int[3]
            {
                SUBMENU_IDS_FILE_OPEN, SUBMENU_IDS_FILE_CLOSE, SUBMENU_IDS_FILE_EXIT
            };
            MENU_IDS_UTILS = new int[2]
            {
                SUBMENU_IDS_DATA_BROWSER, SUBMENU_IDS_UTILS_CYCLOIDAL_GEAR
            };
            MENU_IDS_ROOT = new int[2]
            {
                MENU_ID_FILE, MENU_ID_UTILS
            };
        }

My implementation of Array SubMenuItems(int menuID):

C#:
 public Array SubMenuItems(int menuID)
        {
            switch (menuID)
            {
                case MENU_ID_ROOT: return MENU_IDS_ROOT;
                case MENU_ID_FILE: return MENU_IDS_FILE;
                case MENU_ID_UTILS: return MENU_IDS_UTILS;
            }

            return null;
        }

And bool HasSubMenus(int menuID) follows:
C#:
public bool HasSubMenus(int menuID)
        {
            switch (menuID)
            {
                case MENU_ID_ROOT: return true;
                case MENU_ID_FILE: return true;
                case MENU_ID_UTILS: return true;
            }

            return false;
        }

Numeric menus are not much use - so the menu text has to be defined:
C#:
public string MenuItemText(int menuID)
        {
            switch (menuID)
            {
                case MENU_ID_ROOT: return "Utilities";
                case MENU_ID_FILE: return "File";
                case MENU_ID_UTILS: return "Utils";
                case SUBMENU_IDS_DATA_BROWSER: return "Data Browser";
                case SUBMENU_IDS_FILE_OPEN: return "Open";
                case SUBMENU_IDS_FILE_CLOSE: return "Save & Close";
                case SUBMENU_IDS_FILE_EXIT: return "Save All, Exit";
                case SUBMENU_IDS_UTILS_CYCLOIDAL_GEAR: return "Cycloidal Gear Generator";
            }

            return "";
        }

Finally you have to define what happens when a menu item is selected. Note that this method should return an implementation of IAlibreAddOnCommand. I have not explored that interface yet - so everything here just returns null - seems to work.

C#:
public IAlibreAddOnCommand InvokeCommand(int menuID, string sessionIdentifier)
        {
            var session = alibreRoot.Sessions.Item(sessionIdentifier);

            switch (menuID)
            {
                case SUBMENU_IDS_DATA_BROWSER:
                {
                    return DoDataBrowser();
                }
                case SUBMENU_IDS_FILE_OPEN:
                {
                    return DoFileOpen();
                }
                case SUBMENU_IDS_FILE_CLOSE:
                {
                    return DoFileClose(session);
                }
                case SUBMENU_IDS_FILE_EXIT:
                {
                    return DoFileExit();
                }
                case SUBMENU_IDS_UTILS_CYCLOIDAL_GEAR:
                {
                    return DoCycloidalGear(session);
                }
            }

            return null;
        }


And an example of one of the menu calls..
C#:
 private static IAlibreAddOnCommand DoFileClose(IADSession currentSession)
        {
            currentSession.Close(true);
            return null;
        }

That's it for now

There is quite a lot more to the IAlibreAddOn interface that I have yet to explore - but for now, I can finally start making something useful.
I'm sure there will be more to come - but I want to get the current code working better (fewer bugs) and use Alibre for generating some designs!

db
 

bolsover

Senior Member
I've been digging a little further into the two interfaces IAlibreAddOnCommand and IADAddOnCommandSite.
As noted in an earlier post, calling an AddOn menu item can optionally return an implementation of IAlibreAddOnCommand.

I started by simply implementing the interface and calling against a test menu item:

C#:
using System;
using System.Diagnostics;
using System.Reflection;
using System.Text;
using AlibreAddOn;
using AlibreX;


namespace Bolsover.test
{
    public class TestAddOnCommand : IAlibreAddOnCommand
    {
        private IADSession session;

        public TestAddOnCommand(IADSession session)
        {
            this.session = session;
        }

        /// <summary>
        /// Called to find out if this add-on command is a two-way toggle command
        /// </summary>
        /// <returns></returns>
        public bool IsTwoWayToggle()
        {
            return false;
        }

        /// <summary>
        /// Returns True if add-on wants to show any UI controls in Alibre's left pane window
        /// </summary>
        /// <returns></returns>
        public bool AddTab()
        {
            return false;
        }

        /// <summary>
        /// Called to get the add-on to show its UI inside its special tab page window
        /// </summary>
        /// <param name="hWnd"></param>
        public void OnShowUI(long hWnd)
        {
            Debug.WriteLine("OnShowUI");
        }

        /// <summary>
        /// Called to get the add-on to render its GDI graphics into Alibre's graphics canvas;the origin and size of the view rectangle are passed in.
        /// </summary>
        /// <param name="hDC"></param>
        /// <param name="clipRectX"></param>
        /// <param name="clipRectY"></param>
        /// <param name="clipRectWidth"></param>
        /// <param name="clipRectHeight"></param>
        public void OnRender(int hDC, int clipRectX, int clipRectY, int clipRectWidth, int clipRectHeight)
        {
            Debug.WriteLine("OnRender hDC: " + hDC + ", clipRectX: " + clipRectX + ", clipRectY: " + clipRectY
                            + ", clipRectWidth: " + clipRectWidth + ", clipRectHeight: " + clipRectHeight);
        }


        /// <summary>
        /// Called when left mouse button is clicked
        /// </summary>
        /// <param name="screenX"></param>
        /// <param name="screenY"></param>
        /// <param name="buttons"></param>
        /// <returns></returns>
        public bool OnClick(int screenX, int screenY, ADDONMouseButtons buttons)
        {
            Debug.WriteLine("OnClick X: " + screenX + " Y: " + screenY + " Button: " + buttons);
            return false;
        }

        /// <summary>
        /// Called when left mouse button is double-clicked
        /// </summary>
        /// <param name="screenX"></param>
        /// <param name="screenY"></param>
        /// <returns></returns>
        public bool OnDoubleClick(int screenX, int screenY)
        {
            Debug.WriteLine("OnDoubleClick X: " + screenX + " Y: " + screenY);
            return false;
        }

        /// <summary>
        /// Called when mouse button is depressed; TODO: Describe 'buttons' constants
        /// </summary>
        /// <param name="screenX"></param>
        /// <param name="screenY"></param>
        /// <param name="buttons"></param>
        /// <returns></returns>
        public bool OnMouseDown(int screenX, int screenY, ADDONMouseButtons buttons)
        {
            Debug.WriteLine("OnMouseDown X: " + screenX + " Y: " + screenY + " Button: " + buttons);
            return false;
        }

        /// <summary>
        /// Called when mouse is moved; TODO: Describe 'buttons' constants
        /// </summary>
        /// <param name="screenX"></param>
        /// <param name="screenY"></param>
        /// <param name="buttons"></param>
        /// <returns></returns>
        public bool OnMouseMove(int screenX, int screenY, ADDONMouseButtons buttons)
        {
            Debug.WriteLine("OnMouseMove X: " + screenX + " Y: " + screenY + " Button: " + buttons);
            return false;
        }

        /// <summary>
        /// Called when mouse button is released; TODO: Describe 'buttons' constants
        /// </summary>
        /// <param name="screenX"></param>
        /// <param name="screenY"></param>
        /// <param name="buttons"></param>
        /// <returns></returns>
        public bool OnMouseUp(int screenX, int screenY, ADDONMouseButtons buttons)
        {
            Debug.WriteLine("OnMouseUp X: " + screenX + " Y: " + screenY + " Button: " + buttons);
            return false;
        }

        /// <summary>
        /// Called when use makes a selection change on the editor; actual selection can be obtained using seperate API
        /// </summary>
        public void OnSelectionChange()
        {
          
            if (session.SelectedObjects.Count == 1)
            {
                var proxy = (IADTargetProxy) session.SelectedObjects.Item(0);
                showMessageBoxData(proxy.Target);

            }

        }

        private void showMessageBoxData(object o)
        {
            StringBuilder sb = new StringBuilder();
            PropertyInfo[] infos = o.GetType().GetProperties();
            for (int i = 0; i < infos.Length; i++)
            {
                PropertyInfo info = infos[i];
                try
                {
                    sb.Append(info.Name + ": " + GetPropertyValue(o, info.Name) + "\n");
                }
                catch (Exception e)
                {
                    sb.Append(info.Name + ": returned an exception \n");
                }
                
            }

         //   MessageBox.Show(sb.ToString(), o.GetType().Name, MessageBoxButtons.OK);
            AlibreDataViewer alibreDataViewer = new AlibreDataViewer(o);
            alibreDataViewer.Show();
            alibreDataViewer.TopMost = true;
        }

        public static string GetPropertyValue(object obj, string propName)
        {
            return obj.GetType().GetProperty(propName).GetValue(obj, null).ToString();
        }

        /// <summary>
        /// Called when Alibre terminates the add-on command; add-on should make sure to release all references to its CommandSite
        /// </summary>
        public void OnTerminate()
        {
            Debug.WriteLine("OnTerminate");
        }

        /// <summary>
        /// Called when Alibre has successfully initiated this command; gives it a chance to perform any initializations
        /// </summary>
        public void OnComplete()
        {
            Debug.WriteLine("OnComplete");
        }

        /// <summary>
        /// Called when user holds down the key, passing the keycode as the ASCII value of the key
        /// </summary>
        /// <param name="keycode"></param>
        /// <returns></returns>
        public bool OnKeyDown(int keycode)
        {
            Debug.WriteLine("OnKeyDown:" + keycode);
            return false;
        }

        /// <summary>
        /// Called when user releases the key, passing the keycode as the ASCII value of the key
        /// </summary>
        /// <param name="keycode"></param>
        /// <returns></returns>
        public bool OnKeyUp(int keycode)
        {
            Debug.WriteLine("OnKeyUp:" + keycode);
            return false;
        }

        /// <summary>
        /// Called when escape key is pressed by the user
        /// </summary>
        /// <returns></returns>
        public bool OnEscape()
        {
            Debug.WriteLine("OnEscape");
            return false;
        }

        /// <summary>
        /// Called when mouse wheel is rotated by the user, delta is the magnitude of wheel movement
        /// </summary>
        /// <param name="delta"></param>
        /// <returns></returns>
        public bool OnMouseWheel(double delta)
        {
            Debug.WriteLine("OnMouseWheel: " + delta);
            return false;
        }

        /// <summary>
        /// Called to get the add-on to render its DirectX graphics into Alibre's graphics canvas
        /// </summary>
        public void On3DRender()
        {
            Debug.WriteLine("On3DRender");
        }

        /// <summary>
        /// Sets the command site object on the add-on command
        /// </summary>
        public IADAddOnCommandSite CommandSite { get; set; }

        /// <summary>
        /// Specifies tab name. Needed only if this command returned True when the AddTab method was called
        /// </summary>
        public string TabName { get; }

        /// <summary>
        /// Returns min and max bounding box points of geometry rendered by addon; used for computing front/back clipping planes
        /// </summary>
        public Array Extents { get; }
    }
}

If you look over the above code, it is really just a bunch of Debugging outputs that become visible when I run in my IDE in debug mode- something like:
Code:
On3DRender
Exited Thread 22348
Started Thread 23068
OnMouseMove X: 442 Y: 350 Button: ADDON_NO_BUTTON
On3DRender
On3DRender
OnMouseMove X: 442 Y: 350 Button: ADDON_NO_BUTTON
On3DRender
OnClick X: 442 Y: 350 Button: ADDON_LEFT_BUTTON
On3DRender

I started to get more adventurous with the OnSelectionChange() method when I realised I could use that to get an individual object from the design view.
This led me to work up a property viewer - handy when you want to dig into the internals of a design.
PropertyViewer.jpg

Code for the above is very much 'prototype' at present so I wouldn't recommend testing on production files but as usual, it is available on GitHub
Source code

db
 

Cator

Senior Member
Bolsover,
although abundantly out of the things I can normally do I really appreciate what you are doing. I would like to add some small utilities that I have created and that I use via link link with the add-on tool. Would you be kind enough to post a video in which you explain step by step how to add a simple add-on maybe that refers to a macro through your method? Or if too complex you could put a video in which you explain how to implement your code in Alibre?
Thank you!
Francesco
 

bolsover

Senior Member
Hi Francesco.
I'm away for a few days... and have some more to do on the AddOn code before I'll be able to make a good set of 'how to' instructions.
Once all the exploratory work is done I'll try to put together a very simple sample that might allow others to follow up with their own AddOn.
Kind regards, David
 

Cator

Senior Member
Thanks David for answering me and for your availability and I can't wait to be able to understand from your videos!
Regards,
Francesco
 

bolsover

Senior Member
Something of a breakthrough...
I now have an AddOn with it's own panel in the Alibre design window.. Contents of the window update as I click on different items in the design tree.
I need to do some clean up of the code and more testing before I can share - but will have to wait a week as I'm away for a few days.
Screen grab of the result....
db
Addon panel.jpg
 

NateLiquidGravity

Alibre Super User
Something of a breakthrough...
I now have an AddOn with it's own panel in the Alibre design window.. Contents of the window update as I click on different items in the design tree.
I need to do some clean up of the code and more testing before I can share - but will have to wait a week as I'm away for a few days.
Screen grab of the result....
db
View attachment 35632
That looks like an immensely helpful tool for support to troubleshoot user files with!
[Edit]
How deep does the tree go if you click on Root for example? Do you populate the entire session or just as the user expands it? Just wondering for exploring and performance reasons?
[/Edit]
 
Last edited:

bolsover

Senior Member
Hi Nate
The tree data is retrieved as the user expands nodes.
At present, I flag a node as expandable if it not a 'primitive' type i.e. int, bool, string etc. I don't do any checks if the node object is null - so quite often a node shows as expandable when there is no data.
There are also a number of properties that return exceptions. But for these, I do indicate an exception is thrown. I don't think there is anything I can do about that. It is just the Alibre code returns an exception when it 'maybe' should return null.
The tree goes as deep as you want - just keep clicking! Actually I an thinking of testing an 'expand all' method but I suspect it would lead to an out of memory condition (root references sessions that in turn reference session and root).
Another screen grab - the FacetData has over 25k entries - for a quite simple design.
Screenshot 2022-03-20 174353.jpg
Some of my inspiration for the property viewer actually came while debugging. The debugger offers even more data such as which properties are public/private. I tries to keep things simple though!

David
 

simonb65

Alibre Super User
Nice work David. The FacetData array entries are probably in X,Y,X tuples, so you could display those grouped for ease of reading.
 

bolsover

Senior Member
Hi Simon. Thanks for the comment. TBH, I haven't actually looked into the FacetData. I know it is an Array but no more. I guess it could be structured in some way but have no way to decide other than there does appear to be some pattern to the numbers suggesting an x, y, z tuple. Quite likely that thare are other similar structures in the data. Also worth nothing I don't know if this is static data from the saved file or dynamic, changing as the model is rotated or moved.
Other issues to address in my code first - like how to prevent a duplicate side panel when I select the menu item a second time. I think I need to check for an existing panel before adding a new one - or maybe using the wizogrid trick of having a menu item open\close the panel with successive clicks. Think I'll have a check over the wizogrid and alibre script addons...
 

bolsover

Senior Member
Thanks.. I have quite a lot to add to the project; particularly regarding the IAlibreAddOnCommand implementation - but not had time to tidy up my code. Maybe later this week.
db
 

bolsover

Senior Member
Hi Simon
Sorry for taking some time to respond. A week away and then having some actual work to do...

To answer your question re IAlibreAddOnCommand..
I had previously been ignoring the IAlibreAddOnCommand and was simply making calls from an implementation of IAlibreAddOn to instantiate new tool windows. This approach is probably OK for some classes of tool but if you want tight integration of the tool directly within Alibre you have to use IAlibreAddOnCommand and make reference to IADAddOnCommandSite.

I will not try to get into all the detail here. It is much simpler to reference the code which is available on GitHub

The important classes to examine are: UtilitiesForAlibre and AlibreDataViewerAddOnCommand. these are implementations of IAlibreAddOn and IAlibreAddOnCommand respectively.

There is a compiled copy of the latest code attached here.

There is one significant problem with the Property Data Viewer at present.. The viewer opens correctly when called form the Add-0n menu and closes when the user clicks Close in the viewer window. But if the user selects the tool from the menu without first closing and existing view - well it doesn't work correctly.
I need to implement something more like the WizoGrid menu open/close feature.

As usual latest code is on GitHub

Source code here

A screen grab of the Property Data Viewer in Alibre..
Screenshot 2022-04-04 151740.jpg

David
 

Attachments

  • UtilitiesForAlibre.zip
    830.2 KB · Views: 3

simonb65

Alibre Super User
The viewer opens correctly when called form the Add-0n menu and closes when the user clicks Close in the viewer window. But if the user selects the tool from the menu without first closing and existing view - well it doesn't work correctly.
I need to implement something more like the WizoGrid menu open/close feature.
Been there done it!

The problem is that when the IAlibreAddOn menu click call happens, you have to return a pointer to the IAlibreAddOnCommand you instantiate. That pointer is held within the Alibre framework and is what calls IADAddOnCommand and the framework allocates a CommandSite (IADAddOnCommandSite).

Now, I've found that the only way to set that pointer back to null, and make the framework think the CommandSite is no longer available, is via the menu click handler ...

i.e.

C#:
// toggle the command (and it's panel) on/off
if (m_TestCommand == null)
{
    // show
    m_TestCommand = new TestCommand(this);    // create a new command and pass our reference for access to globals
    m_TestCommand.Session = e.Session;    // pass the session to the command
    return (IAlibreAddOnCommand)m_TestCommand;
}
else
{
    // hide
    m_TestCommand.Close();  // clean up the Command and its CommandSite
    m_TestCommand = null;
    return null;
}

The IADAddOnCommandSite.Terminate() function doesn't seem to totally clean up the framework if you call that from a button handler in the command panel! but I'm still working on a way to do that.
 

simonb65

Alibre Super User
Update:

Pass in the AddOn to your command constructor ...

C#:
private AddOnImplementation m_AddOn = null;
        /// <summary>
        /// Constructor
        /// </summary>
        public TestCommand(AddOnImplementation addon)
        {
            m_AddOn = addon;
        }

Then call m_CommandSite.Terminate() from your close button click handler in the command ...

C#:
private void BtnClose_Click(object sender, EventArgs e)
        {
            m_CommandSite.Terminate();
        }

Then clean up in the OnTerminate() callback and null your command reference in the AddOn ...

C#:
public void OnTerminate()
        {
            Debug.WriteLine("OnTerminate() called");
            if (m_CommandSite != null)
            {
                // clean up
                if (m_handleDockPanel != IntPtr.Zero)
                {
                    m_CommandSite.RemoveDockedPanel((long)m_handleDockPanel);
                    m_handleDockPanel = IntPtr.Zero;
                }
            }


            m_AddOn.m_TestCommand = null;
        }

It may cause a memory leak (to be tested and verified), but it does the functionality you are after.
 

bolsover

Senior Member
Hi Simon
I've not tested with your code but had got things working better..
First I removed the Close button and now rely on the menu selection to toggle the view on/off.

You suggested:
C#:
// toggle the command (and it's panel) on/off
if (m_TestCommand == null)
{
    // show
    m_TestCommand = new TestCommand(this);    // create a new command and pass our reference for access to globals
    m_TestCommand.Session = e.Session;    // pass the session to the command
    return (IAlibreAddOnCommand)m_TestCommand;
}
else
{
    // hide
    m_TestCommand.Close();  // clean up the Command and its CommandSite
    m_TestCommand = null;
    return null;
}

I initially tried something rather like that but found a problem when multiple part files were opened. The IAlibreAddOn implementation is a singleton so we need to keep track of all the IAlibreAddOnCommands open with their respective IADSession objects.

My solution was to use a Dictionary to hold the references:
C#:
/// <summary>
        /// A dictionary to keep track of currently open AlibreDataViewerAddOnCommand object.
        /// </summary>
        private Dictionary<string, AlibreDataViewerAddOnCommand> dataViewerAddOnCommands = new();


        /// <summary>
        /// Toggles the viewer on/off
        /// </summary>
        /// <param name="session"></param>
        /// <returns></returns>
        private IAlibreAddOnCommand DoAlibreDataViewer(IADSession session)
        {
            AlibreDataViewerAddOnCommand alibreDataViewerAddOnCommand;
            if (!dataViewerAddOnCommands.ContainsKey(session.Identifier))
            {
                alibreDataViewerAddOnCommand = new AlibreDataViewerAddOnCommand(session);
                alibreDataViewerAddOnCommand.alibreDataViewer.Visible = true;
                dataViewerAddOnCommands.Add(session.Identifier, alibreDataViewerAddOnCommand);
            }
            else
            {
                if (dataViewerAddOnCommands.TryGetValue(session.Identifier, out alibreDataViewerAddOnCommand))
                {
                    alibreDataViewerAddOnCommand.UserRequestedClose();
                    dataViewerAddOnCommands.Remove(session.Identifier);
                    return null;
                }
            }

            return alibreDataViewerAddOnCommand;
        }


The UserRequestedClose code is now:
C#:
 public void UserRequestedClose()
        {
            alibreDataViewer.Dispose();
            CommandSite.RemoveDockedPanel(DockedPanelHandle);
            DockedPanelHandle = (long) IntPtr.Zero;
            CommandSite = null;
        }

And the OnTerminate..
C#:
public void OnTerminate()
        {
            Debug.WriteLine("OnTerminate");
            if (alibreDataViewer != null) alibreDataViewer.Dispose();
            if (CommandSite != null)
            {
                CommandSite.RemoveDockedPanel(DockedPanelHandle);
                DockedPanelHandle = (long) IntPtr.Zero;
                CommandSite = null;
            }
        }

Whilst the above works, the issue now is that closing a part design without first closing the property view leaves a reference to the closed design in the Dictionary.
If I stick with the code I have now, I need to find a good way of clearing unused objects from the Dictionary.

David
 
Top