Call an ExternalEvent from another assembly.

Call an ExternalEvent from another assembly.

Thomas_LECUPPRE_Let_it_BIM
Advocate Advocate
985 Views
13 Replies
Message 1 of 14

Call an ExternalEvent from another assembly.

Thomas_LECUPPRE_Let_it_BIM
Advocate
Advocate

Hi, this is probably a newbie question but I can't found an answer on internet.
I'm creating a plugin that interact with a database and I'm trying to separate all my UI code to "Revit Code". In this way, I don't need reference to RevitAPI and Revit APIUI dll's in UI part. But here is my problem, how to call my revit event from this UI code ? If I load my UI code assembly in "Revit Code" there is no problem but if I need "Revit Code" in UI Code, I will create a loop.

I know how to do it for a simple WPF project but with event like this, I don't know.
Can someone help me with some explanation or example ?

0 Likes
Accepted solutions (2)
986 Views
13 Replies
Replies (13)
Message 2 of 14

ricaun
Advisor
Advisor

You probably gonna need to create some custom interface on the UI assembly, to inject on the 'Revit Code'.

 

Some ICommand should be useful too.

// Source: https://stackoverflow.com/questions/57379255/close-window-wpf-in-revit
public class RelayCommand : ICommand    
{    
    private Action<object> execute;    
    private Func<object, bool> canExecute;    

    public event EventHandler CanExecuteChanged    
    {    
        add { CommandManager.RequerySuggested += value; }    
        remove { CommandManager.RequerySuggested -= value; }    
    }    

    public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)    
    {    
        this.execute = execute;    
        this.canExecute = canExecute;    
    }    

    public bool CanExecute(object parameter)    
    {    
        return this.canExecute == null || this.canExecute(parameter);    
    }    

    public void Execute(object parameter)    
    {    
        this.execute(parameter);    
    }    
}  

 

See yaa.

Luiz Henrique Cassettari

ricaun.com - Revit API Developer

AppLoader EasyConduit WireInConduit ConduitMaterial CircuitName ElectricalUtils

0 Likes
Message 3 of 14

Thomas_LECUPPRE_Let_it_BIM
Advocate
Advocate

Thank you for your answer. Of course I will need this but it is the point where my current knowledge stop. I'm asking for help because I don't know how to do that. If you can provide me some example, sources, or article, I will be happy.

For this ICommand part, I already use this in my code, because of MVVM pattern.

EDIT :

Other thing, I try to not use Revit API and APIUI  as dependcies in 'UI Code'. Because these Dll depend of the Revit verison (wich depend of the revit version install on final user computer).

0 Likes
Message 4 of 14

Kennan.Chen
Advocate
Advocate
Accepted solution

You need to define interfaces to use dependency inversion or IoC to create a loosely-coupled architecture.

For example, if you want to show all the family names in your ui, you may do it like this:

KennanChen_0-1644462834898.png

 

 

// plugin
public class Command : IExternalCommand
{
    public Result Execute(ExternalCommandData commandData)
    {
        var window = new MyWindow(new FamilyService(commandData.Application));
        window.ShowDialog();
        return Result.Succeeded;
    }
}
public class FamilyService : IFamilyService
{
    private UIApplication _app;
    public FamilyService(UIApplication app)
    {
        this._app = app;
    }
    public string[] GetFamilyNames()
    {
        return new FilteredElementCollector(this._app.ActiveUIDocument.Document).OfType(typeof(Family)).Cast<Family>().Select(family => family.Name).ToArray();
    }
}

// ui lib (not depending on plugin and revit api)
public interface IFamilyService
{
    string[] GetFamilyNames();
}
public class MyWindow : Window
{
    private string[] FamilyNames;
    public MyWindow(IFamilyService familyService)
    {
        this.FamilyNames = familyService.GetFamilyNames();
        // initialize ui
    }
}

 

 

Actually, Revit itself leverages the same design pattern, the IExternalCommand, IExternalApplication, IExternalEventHandler etc are all provided for dependency inversion.

 

Hope it helps!

Message 5 of 14

Thomas_LECUPPRE_Let_it_BIM
Advocate
Advocate

Hi, I see what you mean but it's not really what I'm look for. To be more explicit, I don't want to see Autodesk Revit dll as references in my UI project. I want to call the UI from an external dll and come back to revit "brain" to execute my external event.

Would I be able to to this if I use your package in VS @Kennan.Chen. I saw this on an old post in this forum and visit your github 🙂

0 Likes
Message 6 of 14

Kennan.Chen
Advocate
Advocate

Hi, I don't see any mismatch between what I said and what you want. Maybe I just failed to express my ideas correctly as a none-native English speaker.

Basically, you want to decouple your UI library with Revit so that you can initialize the same UI across different products.

I actually did the same work in a component library project. The project outputs a Revit addin, a standalone desktop application and a plugin for a self-owned design product like Revit. They all share the same UI.

To make it work, any reference to a specific platform is not allowed in the UI library.

A simple solution is to design multiple interfaces(IFamilyService .etc) and data types(IFamilyData .etc) to abstract the business logic right inside the UI library and then implement those interfaces in the three products with platform specific data and APIs.

In a word, if A cannot be directly used by B, abstract it  in B and create a shell for A to match the abstraction and inject the shell to B.

Message 7 of 14

Thomas_LECUPPRE_Let_it_BIM
Advocate
Advocate

I think I don't have the knowledge to this for now but I will try. I know where I want to go but don't know how for now. Do you have an link to read more about this ?

EDIT : Can you share with me the tree structure of your Revit addin, standalone desk app ans plugin ? I'm trying you solution but don't know how to organize it correctly.

0 Likes
Message 8 of 14

Kennan.Chen
Advocate
Advocate

Hi, I created a demo project to show the ideas. See the attachment of this comment.

The demo shows only the basic ideas of IoC. For more complicated business, the container technology should be involved to better manage dependencies.

Note that I didn't have a Revit environment to run a single test and it's been a long time since my last Revit programming, which means the code may have a big chance to fail at runtime.

Just read the code and try to fix any runtime error yourself.

Hope it helps!

 

Edit: I update the demo to include a WPF standalone application to complete the solution.

Another word, comparing this solution with a standard or a protocol in real world(like different kinds of physical ports on electronic devices), the interface defined in the UI lib is a kind of protocol that the UI requires for communicating with outside world without knowing any detail of that world.

Message 9 of 14

Thomas_LECUPPRE_Let_it_BIM
Advocate
Advocate

What the application should show when opened ? The window appears but nothing in it. In what context in Revit (like open it in project, family. With or without family load in it ?) should I start it ?

0 Likes
Message 10 of 14

Kennan.Chen
Advocate
Advocate

The window shows a list of families in the currently open project

0 Likes
Message 11 of 14

Thomas_LECUPPRE_Let_it_BIM
Advocate
Advocate

Hi, I have worked as you show me but I got a last problem, Revit seems not allow me to start my plugin because it can't access dependency.

To be able to update my programme without restarting revit I use this :

 

        public Result Execute(ExternalCommandData commandData, ref string message, Autodesk.Revit.DB.ElementSet elements)
        {
            try
            {
                string assemblyPath = @"C:\ProgramData\Letitbim\ProfilMaker.dll";
                string assemblyType = "ProfilMaker.Command";
                string methodName = "Execute";

                byte[] assemblyBytes = File.ReadAllBytes(assemblyPath);

                Assembly dataAssembly = Assembly.Load(assemblyBytes);
                Type type = dataAssembly.GetType(assemblyType);
                var instance = Activator.CreateInstance(type);
                var method = type.GetMethod(methodName);

                method.Invoke(instance, new object[] { commandData, message, elements });
            }
            catch (Exception ex)
            {
                MessageBox.Show($"{ex.Message}\n\n{ex.InnerException}\n\n{ex.Source}\n\n{ex.StackTrace}");
            }

            return Result.Succeeded;
        }

 

 

from the same dll that generate my button in Revit application. (look like : app.cs (code that create button etc), every button point to mybutton.cs and execute code above. app.cs and every mybutton.cs are build in the same .dll file)

I already explain my problem here. Consider that A is Revit plugin part (command.cs) and B is my ui.

 

Thank for futur help.

0 Likes
Message 12 of 14

Kennan.Chen
Advocate
Advocate

You can subscribe to the AppDomain.AssemblyResolved event to manually resolve the missing assembly when the AppDomain fails to resolve some assembly automatically.

Message 13 of 14

Thomas_LECUPPRE_Let_it_BIM
Advocate
Advocate

Is that because my dependencies (ui DLL files) isn't load in "app" dll that I can't success that ?
I read AppDomain.AssemblyResolve Microsoft docs, but without clearly understand what it does, it is difficult to apply this to my code 🙂

Message 14 of 14

Thomas_LECUPPRE_Let_it_BIM
Advocate
Advocate
Accepted solution

Ok I got it.
First I subscribe to AppDomain.AssemblyResolve event like this :

 

AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
private Assembly ResolveAssembly(object sender, ResolveEventArgs args)
{
      string name = args.Name.Split(',')[0];
      return Assembly.LoadFile(@"C:\YourPath\" + name + ".dll");
}

 

 

Because my system is french culture, I had some trouble with this. So by checking the culture of the the assembly before sending it, I return null if assembly isn't neutral culture and that work !

Final method :

 

private Assembly ResolveAssembly(object sender, ResolveEventArgs args)
{
      string name = args.Name.Split(',')[0];
      string culture = args.Name.Split(',')[2];
      // Si la culture n'est pas neutre alors on passe.
      if (name.EndsWith(".resources") && !culture.EndsWith("neutral")) return null;

      return Assembly.LoadFile(@"C:\YourPath\" + name + ".dll");
}

 

 

0 Likes