Problem with modeless Form Window, which implements IWin32Window

Problem with modeless Form Window, which implements IWin32Window

lwlXUTTT
Advocate Advocate
1,314 Views
14 Replies
Message 1 of 15

Problem with modeless Form Window, which implements IWin32Window

lwlXUTTT
Advocate
Advocate

Dear Community,

 

I tried to follow the logic, described on Jeremy's post here:

https://jeremytammik.github.io/tbc/a/0088_revit_window_handle.htm

 

First - what is a goal.

The goal is to create a Win Form window, which would start with Revit Application and close with Revit Application. Window is in modeless dialog and displays the output of different actions/commands which are happening while working in Revit Document. Everything works fine for me, except for the part, that with each Revit Command (implementing IExternalCommand) new instance of Form Window is created. I would like to keep only one window which hides and shows, whenever any action is taken.

Below my code - I hope anyone could be able to direct me which path to go....

 

Win Form Class:

public partial class CmdWindowHandleForm : Form
{
        public string LabelText
        {
            get{ return _labelText.Text; }
            set{ _labelText.Text = value; }
        }

        private static CmdWindowHandleForm _thisInstance;

        public CmdWindowHandleForm()
        {
            InitializeComponent();
        }
}

partial class CmdWindowHandleForm
{
	private void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();

            this._labelText = new Label();
            this._labelText.Text = string.Empty;

            this.Controls.Add(this._labelText);
        }
}

 

Class with WindowHandle, which is used as argument when Form.Show() method is called:

public class WindowHandle : IWin32Window
{
        public IntPtr Handle
        {
            get{ return _handleWindow; }
        }

        IntPtr _handleWindow;

        public WindowHandle(IntPtr handleWindow)
        {
            _handleWindow = handleWindow;
        }
}

 

And lastly my static class of InfoConsole window, which would run along Revit app:

public static class InfoConsole
{
        public static CmdWindowHandleForm Window
        {
            get{ return _window; }
        }

        static WindowHandle _hWndRevit = null;
        static CmdWindowHandleForm _window = null;

        public static void Show(string message)
        {
            if (_hWndRevit == null)
            {
                Process process = Process.GetCurrentProcess();

                IntPtr h = process.MainWindowHandle;
                _hWndRevit = new WindowHandle(h);
            } 
            if(InfoConsole._window == null)
            {
                _window = new CmdWindowHandleForm();
                _window.Show(_hWndRevit as IWin32Window);
            }
            else
            {
                _window.Visible = true;
            }
            _window.AddText(message);
        }
}

 

And while running a command (implementing IExternalCommand) method is simply called:

InfoConsole.Show(outputMessage);

 

any help much appreciated,

Lukasz

 

0 Likes
1,315 Views
14 Replies
Replies (14)
Message 2 of 15

ricaun
Advisor
Advisor

First of all this implementation looks a little old. (Revit 2009 looks dope)

 

By looking at your static InfoConsole class, looks like everything is correct to only open one Window.

 

How are you testing your IExternalCommand?

 

if you are using an old version of AddinManeger to test the command this could be the issue, some version of the AddinManeger does not work with static. That would explain a new Window every time the command is executed.

 

PS: You should try WPF instead of Form.

 

Luiz Henrique Cassettari

ricaun.com - Revit API Developer

AppLoader EasyConduit WireInConduit ConduitMaterial CircuitName ElectricalUtils

0 Likes
Message 3 of 15

lwlXUTTT
Advocate
Advocate

Hello Luiz,

 

So, I have tested the same method fired through:

-class implementing IExternalApplication - everything works!

-class implementing IExternalCommand - problem exists, every time command is used (assigned to button in ribbon) new instance of Form Window is created.

 

IExternalCommand is defined in .DLL which is not an addin loaded to Revit. I am using Byte Array strategy described by Jeremy here:

http://jeremytammik.github.io/tbc/a/0953_reload_debug_cmd.htm

 

Can this be an issue ?

0 Likes
Message 4 of 15

mhannonQ65N2
Collaborator
Collaborator

If you are reloading the dll every time you execute the command, then yes, that is the problem.

0 Likes
Message 5 of 15

ricaun
Advisor
Advisor

IExternalCommand is defined in .DLL which is not an addin loaded to Revit. I am using Byte Array strategy described by Jeremy here:

http://jeremytammik.github.io/tbc/a/0953_reload_debug_cmd.htm

 

Can this be an issue ?

 

Yes, that's the reason.

If your command is loading a new dll every time the command is executed, static variables does not gonna works because you are 'reseting' the assembly.

 

You could use my plugin AppLoader to run commands and application on the fly in Revit.

 

Usually is a bad idea to use Assembly.Load if you don't know the consequences. 🤗

 

Luiz Henrique Cassettari

ricaun.com - Revit API Developer

AppLoader EasyConduit WireInConduit ConduitMaterial CircuitName ElectricalUtils

0 Likes
Message 6 of 15

jeremy_tammik
Alumni
Alumni

I answered your question on StackOverflow:

  

   

The code you refer to is way out of date. It was published by The Building Coder in February 2009:

  

  

Access to the Revit main window handle changed in  2018:

  

  

Jeremy Tammik Developer Advocacy and Support + The Building Coder + Autodesk Developer Network + ADN Open
0 Likes
Message 7 of 15

lwlXUTTT
Advocate
Advocate

Hello @jeremy_tammik ,

Hello @ricaun ,

Hello @mhannonQ65N2 ,

 

Thank you very much for your input. I have tested few more options and I would like to share with you the results.

First of all, I tested windows, that were created both in Forms and WPF - in both cases, results are unsatisfactory, which means that with every IExternalCommand fired, new instance of "Console" window is created.

I thought that maybe creating the static object of Window during OnStartup event in MainApplication class (implmenting IExternalApplication) would solve the problem, but it looks like firing IExternalCommand afterwards completely "resets" static object of my Window to null.

 

As mentiond above by you, that problem may be in loading "Flesh" .dll through "Trigger" .dll (Byte Array approach)

 

Below implementation:

 

public class MainApplication : IExternalApplication
{
	static ConsoleWindow _console;
	public static ConsoleWindow Console
        {
            	get { return _console; }
        }

	public Result OnStartup(UIControlledApplication uiApp)
        {
		_console = new ConsoleWindow();
	}
}

public static class InfoConsole
{
	public static void Show()
	{
		MainApplication.Console.Show();
	}
}

 

Secondly, my apologies for an incomplete research into updates of modal/modeless windows with Revit as owner...

I have updated the implementation posted above in the thread, however the problem is the same as in the implementation above - static values specified for MainApplication object are being reset to null.

 

Test 2.1 - static UIControlledApplication object in MainApplication class:

public class MainApplication : IExternalApplication
{
	static UIControlledApplication _uiApp;
	public static UIControlledApplication UiApp
        {
            get { return _uiApp; }
        }

	public Result OnStartup(UIControlledApplication uiApp)
        {
		_uiApp = uiApp;
	}
}

public static class InfoConsole
{
	static CmdWindowHandleForm _window;
	static WindowHandle _hWndRevit;

	public static void Show()
	{
		if (_hWndRevit == null)
		{
			IntPtr h = MainApplication.UiApp.MainWindowHandle;
			_hWndRevit = new WindowHandle(h);              
		}
		_window.Show(_hWndRevit as IWin32Window);
	}
}

No window apprears, since value for MainApplication.UiApp is null while running IExternalCommand. (was somehow reset)

 

Test 2.2 - instance of ExternalCommandData object feed into InfoConsole class:

public static class InfoConsole
{
	static CmdWindowHandleForm _window;
	static WindowHandle _hWndRevit;

	public static void Show(ExternalCommandData commandData)
	{
		if (_hWndRevit == null)
		{
			IntPtr h = commandData.Application.MainWindowHandle;
			_hWndRevit = new WindowHandle(h);              
		}
		_window.Show(_hWndRevit as IWin32Window);
	}
}

As a result, with every usage of IExternalCommand new instance of my "Console" Window is created.

 

If you have any idea of direction, in which I can navigate my investigation further, I would be more than grateful for your invaluable input,

CHeers, Lukasz

0 Likes
Message 8 of 15

jeremy_tammik
Alumni
Alumni

Dear Lukasz, 

  

Thank you for sharing your results so far. 

  

Yes, I definitely have a clear idea of direction. Many samples demonstrating how to set up dockable panels with WPF controls running in a modeless context and implementing an external command to access a valid Revit API context and interact with the Revit API within that have been published and discussed. I would suggest that you take one of the working samples, set it up to run in your environment, and migrate your code into it. 

  

One such starting point is the official Revit SDK sample ModelessForm_ExternalEvent. You can also pick samples from the lists of articles on External Events for Modeless Access and Driving Revit from Outside or dockable panels:

  

  

I also implemented several applications driving Revit from the cloud that can also show you how to set up the external event so that it can be driven from a console application or any other source in the universe:

  

  

I hope this helps and look forward to seeing how you end up solving this.

  

Good luck and best regards,

  

Jeremy

  

Jeremy Tammik Developer Advocacy and Support + The Building Coder + Autodesk Developer Network + ADN Open
Message 9 of 15

lwlXUTTT
Advocate
Advocate

Good evening Jeremy,

 

Thank you for your response and hints. I think I am heading in the right direction, I have managed to implement the DockablePane apprach quite painlessly, however I am stuck at one point, and again - I hope you will be able to direct me toward the solution.

Firstly - the implementation:

 

//WPF class:
public ConsoleWindow : Page, IDockablePaneProvider, INotifyPropertyChanged
{
	public string BoundTextinXAML {get; set;}
	....
	public void SetupDockablePane(DockablePaneProviderData data)
	{
		...
	}

	public void AddNewText(string newText)
	{
		BoundTextinXAML += newText;
	}
}

//Static class that executes WPF window
public static class InfoConsole
{
        const string guid = "{----<my GUID>-----}";

	//method executed only OnStartup
        public static Result Register(UIControlledApplication uiApp)
        {
            if (uiApp == null) { return Result.Failed; }

            DockablePaneProviderData data = new DockablePaneProviderData();
            ConsoleWindow console = new ConsoleWindow();
            DockablePaneId dpid = new DockablePaneId(new Guid(guid));
            uiApp.RegisterDockablePane(dpid, "MyConsole", console as IDockablePaneProvider);

            return Result.Succeeded;
        }
    
	//Method fired on demand
        public static void Show(string message, object data)
        {
		DockablePaneId dpid = new DockablePaneId(new Guid(guid));
		DockablePane dp = null;

		if (data is UIControlledApplication)
			dp = (data as UIControlledApplication).GetDockablePane(dpid);
		else if (data is ExternalCommandData)
			dp = (data as ExternalCommandData).Application.GetDockablePane(dpid);

	
         //Here I am somehow lost
		ConsoleWindow consoleWnd = dp.GetFrameworkElement()* as ConsoleWindow //how to achieve this?
		consoleWnd.AddText(message);

		if (dp != null)
		dp.Show();
        }
}

 

 

So, what is clear to me, that regardless of using Byte Array apprach to read "external or unloaded" .DLLs via "loaded" .DLL (addin), the registration of DockablePane ist consistent, in the sense that the same DockablePane can be retrieved at any time, as long as Application instance is running. (unlike assigned static varaibles, which loose its values ). This satisfies me completely.

 

However, I cannot find a way to retrieve my instance of Page class from DockablePane, so properties bound in XAML can be updated...(like in code above). Is there any solution/workaround for this? (I hope I am not offtopic...)

 

Best Regards / Freundliche Grüsse,

Lukasz

 

0 Likes
Message 10 of 15

jeremy_tammik
Alumni
Alumni

Your ConsoleWindow class is derived from Page. So, create and maintain a singleton instance to you ConsoleWindow object and access that. There is even a pattern for that:

  

https://duckduckgo.com/?q=.net+singleton

  

Jeremy Tammik Developer Advocacy and Support + The Building Coder + Autodesk Developer Network + ADN Open
0 Likes
Message 11 of 15

lwlXUTTT
Advocate
Advocate

Dear Jeremy,

 

Thank you for the hint with Singletons.

Unforntunately, this solution haven't worked for me - I have tested the implementation and below are my findings:

  • when IExternalApplication methods are executed, static values of objects are preserved (including Singleton for ConsoleWindow)
  • when IExternalCommands are executed, all static values are reset (including Singleton for Console Window) - in the sense that with every IExternalCommand executed, new values to static properties are assigned.

 

Below Singleton implementation of ConsoleWindow class:

 

 

public partial class ConsoleWindow : Page, INotifyPropertyChanged, IDockablePaneProvider
{
   public static ConsoleWindow Instance
   {
      get
      {
         lock (lockThis)
         {
            if (_instance == null)
               _instance = new ConsoleWindow();

            return _instance;
         }
      }
   }

   public string TextBoundinXAML
   { ... }

   private static ConsoleWindow _instance = null;
   private static object lockThis = new object();

   private ConsoleWindow()
   {
      InitializeComponent();
      MainGrid.DataContext = this;

      //here I can see that creation date of window is not preserved, 
      //and new instance is created with every IExternalCommand executed
      _textBoundinXAML = "Console Initialized. Date: "+DateTime.Now.ToString("yyyy-MM-dd / HH:mm:ss")+"\n"; ;
   }

   public void SetupDockablePane(DockablePaneProviderData data)
   {...}
}

 

 

 

My suspicion is that it is caused by Byte Array appoach of DLL loading (as mentioned before and also pointed out by @ricaun in the thread above), based on your solution:

http://jeremytammik.github.io/tbc/a/0953_reload_debug_cmd.htm

 

On the contrary, DockablePane is consistent and preserve its "existance" of only one instance (defined OnStartup) as long as application is running, without creating new instances.  So two questions are still bothering me:

  • is there another way of accessing FrameElement from registered DockablePane ?
  • is it possible to somehow "register" FrameworkElement/ConsoleWindow in Revit context, simirarly to registering DockablePane (e.g. with GUID)?

Looking forward for your reply,

Best, Lukasz

 

0 Likes
Message 12 of 15

jeremy_tammik
Alumni
Alumni

If the dockable panel is persistent, you just have to associate your singleton object with the appropriate instance of that class. I think if you read a bit more and meditate on the whole situation, the proper solution will come to you. You seem to have all the pieces in place. Just wait for them to settle a bit.

  

Jeremy Tammik Developer Advocacy and Support + The Building Coder + Autodesk Developer Network + ADN Open
0 Likes
Message 13 of 15

lwlXUTTT
Advocate
Advocate

Dear Jeremy,

 

I have investigated your suggestion, implemented it and also I have tried two more possible paths of solution, however the result is still the same:

  • as long as methods are exectued by events registered by App OnStartup - eveything works fine, in the sense that the same (persistant) DockablePane can be obtained and also the same (persistant) Singleton instance of ConsoleWindow (inherited from Page) can be obtained and its properties updated.
  • when methods are executed by IExternalCommand, the same (persistant) DockablePane can be obtained but not the same Singleton instance anymore... when IExternalCommand is executed all static values assigned OnStartup by IExternalApplication are reset to null - hence, with every IExternalCommand executed new Singleton instance is created.

Below implementation of solution, which associates Singleton Page object with instance of registered DockablePane object.

WPF Control Class - ConsoleWindow

 

public partial class ConsoleWindow : Page, INotifyPropertyChanged, IDockablePaneProvider
{
   public static ConsoleWindow Instance
   {
      get
      {
         lock (lockObject)
         {
            if (_instance == null)
            {
               _instance = new ConsoleWindow();
            }
            return _instance;
         }
      }
   }

   private static ConsoleWindow _instance = null;
   private static readonly object lockObject = new object ();

   public ConsoleWindow()
   {
      InitializeComponent();
      MainGrid.DataContext = this;
   }

   public void SetupDockablePane(DockablePaneProviderData data)
   {
      data.FrameworkElement = ConsoleWindow.Instance as FrameworkElement;
   }
}

 

 

 static class InfoConsole class, used to register DockablePane OnStartup and to Show DockablePane on demand:

 

public static class InfoConsole
{
   static DockablePane _dp;
   const string guid = "{<---------GUID--------->}";

   public static Result Register(UIApplication uiApp)
   {
      if (uiApp == null) { return Result.Failed; }

      DockablePaneProviderData data = new DockablePaneProviderData();
      DockablePaneId dpid = new DockablePaneId(new Guid(guid));
      uiApp.RegisterDockablePane(dpid, "Console", ConsoleWindow.Instance as IDockablePaneProvider);

      return Result.Succeeded;
   } 

   public static void Show(string message, UIApplication uiApp)
   {
      DockablePaneId dpid = new DockablePaneId(new Guid(guid));
      DockablePane dp = null;
         
      dp = uiApp.GetDockablePane(dpid);
      _dp = dp;

      if (ConsoleWindow.Instance != null)
         ConsoleWindow.Instance.AddLabel(message);

      dp?.Show();             
   }
}

 

DockablePane is registered after application is initialized:

 

private static void OnApplicationInitialized(object sender, Autodesk.Revit.DB.Events.ApplicationInitializedEventArgs e)
{
   InfoConsole.Register(UiApp);
}

 

 

Implementation above also exposes the second path, which I investigated - the considerations about the starting point from which, the methods are executed, and also from where DockablePane is retrieved. In case of OnStartup events, it is UIControlledApplication object and in case of IExternalCommand it is UIApplication object. So trying to have the same starting point, during OnStartup event I retrieved UIApplication object from UIControlledApplication object and used the first one, both for registering and calling(showing) DockablePanes.

 

Below implementation of UIApplication Retrieve using Reflection:

 

public Result OnStartup(UIControlledApplication uiControlledApp)
{
   _uiControlledApp = uiControlledApp;

   Type type = uiControlledApp.GetType();

   string propertyName = "m_uiapplication";
   BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance;

   object result = type.InvokeMember(propertyName, flags, null, uiControlledApp, null);

   _uiApp = (UIApplication)result;
}

 

 

Third path I followed was to make use of ExtensionMethods to create a sort of workaround for ExtensionProperty - in my case would it be Property ConsoleWindow assigned to DockablePane object, so the WPF Page can be easily obtained directly from DockablePane instance. Below implementation of ExtensionClass:

 

public class DockablePaneProperties
{
   public ConsoleWindow ConsoleWindow { get; set; } 
}

public static class DockablePaneExtended
{
   public static readonly Dictionary<DockablePane, DockablePaneProperties> Data 
   = new Dictionary<DockablePane, DockablePaneProperties>();

   public static DockablePaneProperties GetProperty(this DockablePane dp)
   {
      if(!Data.TryGetValue(dp, out var value))
      {
         Data[dp] = new DockablePaneProperties();
         return Data[dp];
      }

      return value;
   }
}

 

 

As a Postface, I would like to add that all of the implementations above work in the "standard" scenario, so there is one DLL. file loaded and locked by Revit App. However in my case, it is crucial that I use Reflection to read constantly developed DLL (not locked) by "Trigger" DLL loaded to Revit (locked) - Byte Array reading.

If you think that investigating Singletons further, would still pay off, or have any other suggestions - I would be enormously grateful for the feedback,

 

Best,

Lukasz

0 Likes
Message 14 of 15

jeremy_tammik
Alumni
Alumni

Make the singleton instance a member variable of your external application, and set its status from the external command. I implemented a system like that in my round-trip cloud editor, I think:

  

https://github.com/jeremytammik/RoomEditorApp/tree/master/RoomEditorApp

  

Above all, keep it simple!

  

Jeremy Tammik Developer Advocacy and Support + The Building Coder + Autodesk Developer Network + ADN Open
0 Likes
Message 15 of 15

lwlXUTTT
Advocate
Advocate

Dear Jeremy,

 

Thank you for your suggestion. Unfortunately it also didn't work out - but I have found the genesis of the problem.

The problem lies in my implementation of loading Assembly into Byte Array. With my implementation, every use of External Command loaded new Dll, therefore the persistance of defined values for class member could not be preserved.

Partial solution would be that Dll would be loaded only once OnAppStartup - in this case all my implementation above work.

Since this topic is now related to another topic, which I started in previous thread, I would like to close this thread here and move out conversation back there:

https://forums.autodesk.com/t5/revit-api-forum/passing-document-instance-as-argument-to-external-exe...

Disussion moved to another thread

 

Cheers, Lukasz

0 Likes