Revit API Forum
Welcome to Autodesk’s Revit API Forums. Share your knowledge, ask questions, and explore popular Revit API topics.
cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

ExternalEvent.Raise() should accept parameters

24 REPLIES 24
SOLVED
Reply
Message 1 of 25
luigi.trabacchin
3678 Views, 24 Replies

ExternalEvent.Raise() should accept parameters

I know this can be achieved in other ways, but i want to discuss the fact that this would be much understandable for developers ...

 

would be nice to have generics involved here having an interface like this:

 

IExternalEventHandler<T>


ExternalEvent<T> EE =  ExternalEvent.Create(handler IExternalEventHandler<T>);
EE.Raise(T obj);

 

24 REPLIES 24
Message 2 of 25

Luigi:

 

Perhaps it would be nice and more convenient if the external events used generics. However, the Revit API being what it is, that is a native C++ core where the functionality sits underneath a very thin shell of managed C++ wrapping it (to allow access to it for external .NET developers), incorporating generic tends to be cumbersome and typically too expensive, thus rarely worth the effort. Since external events can be easily and quite straightforwardly used without generic, I do not see us (Revit R&D) investing to generic in this particular area any time soon.

Thank you for understanding.

Arnošt Löbel
Message 3 of 25

Thanks for the reply Arnost!

Please consider for future version at least the idea of passing an object to raise() method!
Thanks again!

Message 4 of 25

Thanks for the suggestion, Luigi, but if I may be brutally honest, as I often am, I'd have to admit that we would be unlikely making that change. I understand that in an ideal environment a generic argument passed to the Raise method could be an added convenience, but I do not believe it improves the overall experience all that much. You are free to disagree, naturally. My view is as follows: There is always only one Custom Event object linked to one Handler. Both the Event and the Handler are owned by the same programmer/application, which raises the event as well as it handles it. Why not to customize the Handler to one's liking? Before the event is raised the programmer can easily set whatever data on the handler, so the handler has the data ready when it is invoked by Revit. Since an external event can only be raised once at a time, there is also only one invocation of the handler per each raised event - so there is fat chance (or should be) the data in the handler changes between the time an event is raised and later handled. If that is still not simple enough, there is always the option of adding a generic method to the Handler. After all, a handlers is the user's class thus the user can design it any way he or she likes. I can easily imagine a generic method on the Handler: Handler<T>.Raise(T data). If you make it so that the Handler owns the ExternalEvent instance, then the Handler can store the T argument, invoke m_event.Raise(), and wait for being called.
 
Cheers

Arnošt Löbel
Message 5 of 25

I don't know the impact it could have on the background, i do only managed code, i think this would help developers better understand how to use this (making it more similar to events in .net).
Using it like that sounds a little bit hacky.

 Just my two cents, thanks anyway!

Message 6 of 25

I am not saying I do not see where you are coming from, Luigi. If there was a parameter to the Raise method, it would indeed make it more similar to standard events’ behavior. However, the similarity of External Events with typical events has been something we actually haven’t wanted to highlight too much. The reason is that External Events are not typical events in many ways, thus we did not want to create the illusion they will be behaving the same.

 

In fact, we use the External Events name only for a lack of a better term. If we wanted to name them to more closely describe their behavior, we would have probably used something like “Asynchronous Command” or “Action Request” and alike. But we did not want to make it too complicated (and those names would have their set of issues too.)

 

The following are the two most pronounced differences between External Events and standard events:

  1. While typical events have (or at least could have) many handlers, there is always only one handler of an single external event.
  2. For typical events, the owner of an event is different from its handler. In external events, however, the Owner is also the Handler.

 

So, for the two reasons I have just listed, it is indeed necessary to have an argument with which an event is raised. It simply cannot work without it, because an event’s handler is completely separated from the owner, plus there is potentially many of them. In External Events, on the other hand, a passing along an argument may be confusing for some, since the argument will end up exactly where the event was raised from. (Plus it is not exactly a piece of cake to implement it, as I said earlier.)

Arnošt Löbel
Message 7 of 25

Attached the data you wish here, like this. You'll need a  getset; of course event method. 

 

handler_event = new ExternalEventMy() {MentionThis = thatClass};

 

 

And these are not copied therefore you can change it anywhere in your project before you hit the raise method.

            thatClass.Test1 = "nothing new here";
            exEvent.Raise();
Message 8 of 25

Dear Joshua,

 

Happy New Year to you and the entire Revit API community!

 

Thank you for your good suggestion.

 

Can you flesh it out just a little bit more for less experienced programmers?

 

If you provide a minimal compilable sample I can promote it to a blog post as well for better readability.

 

Thank you!

 

Cheers,

 

Jeremy



Jeremy Tammik
Developer Technical Services
Autodesk Developer Network, ADN Open
The Building Coder

Message 9 of 25
cyril
in reply to: jeremytammik

Hello,

@arnostlobelyou are talking about modifying the handler. Except if I missed something you cannot modify it after the ExternalEvent is created.

 

@jeremytammikI don't know if joshua.lumley answered you but I think I did something similar in python. Not a perfect solution but well enough until I find better. See my articlefull code sample and the following extract :

 

class CustomizableEvent:
def __init__(self):
self.function_or_method = None
self.args = ()
self.kwargs = {}

def raised_method(self):
"""
Method executed by IExternalEventHandler.Execute when ExternalEvent is raised by ExternalEvent.Raise.
"""
self.function_or_method(*self.args, **self.kwargs)

def raise_event(self, function_or_method, *args, **kwargs):
"""
Method used to raise an external event with custom function and parameters
Example :
>>> customizable_event = CustomizableEvent()
>>> customizable_event.raise_event(rename_views, views_and_names)
"""
self.args = args
self.kwargs = kwargs
self.function_or_method = function_or_method
custom_event.Raise()


customizable_event = CustomizableEvent()


# Create a subclass of IExternalEventHandler
class CustomHandler(IExternalEventHandler):
"""Input : function or method. Execute input in a IExternalEventHandler"""

# Execute method run in Revit API environment.
# noinspection PyPep8Naming, PyUnusedLocal
def Execute(self, application):
try:
customizable_event.raised_method()
except InvalidOperationException:
# If you don't catch this exeption Revit may crash.
print "InvalidOperationException catched"

# noinspection PyMethodMayBeStatic, PyPep8Naming
def GetName(self):
return "Execute an function or method in a IExternalHandler"


# Create an handler instance and his associated ExternalEvent
custom_handler = CustomHandler()
custom_event = ExternalEvent.Create(custom_handler)

 

Please forgive my code style. I'm not pro.

 

Message 10 of 25
markvu94ZVF
in reply to: cyril

You can work around this by wrapping the Revit event. For example, you can do the following:

 

abstract public class RevitEventWrapper<T> : IExternalEventHandler 
{
    private object @lock;
    private T savedArgs;
    private ExternalEvent revitEvent;
 
    public RevitEventWrapper() 
    {
        revitEvent = ExternalEvent.Create(this);
        @lock = new object();
    }
 
    public void Execute(UIApplication app) 
    {
        T args;
 
        lock (@lock)
        {
            args = savedArgs;
            savedArgs = default(T);
        }
 
        Execute(app, args);
    }
 
    public string GetName()
    {
        return GetType().Name;
    }
 
    public void Raise(T args)
    {
        lock (@lock)
        {
            savedArgs = args;
        }
 
        revitEvent.Raise();
    }
 
    abstract public void Execute(UIApplication app, T args);
}

 

 Then, you can implement a handler that takes arguments, like so:

public class EventHandlerWithStringArg : RevitEventWrapper<string>
{
    public override void Execute(UIApplication uiApp, string args)
    {
        // Do your processing here with "args"
    }
}

Finally, you can raise your event like this

 

EventHandlerWithStringArg myEvent = new EventHandlerWithStringArg();
.
.
.
myEvent.Raise("this is an argument");

 

There are threading pitfalls, of course, but these are outside the scope of this answer, this is the best you can do given Revit current architecture

 

Message 11 of 25
joshua.lumley
in reply to: markvu94ZVF

Brilliant! (and obvious in retrospect but that's the way with all coding)

Message 12 of 25

"so there is fat chance (or should be) the data in the handler changes between the time an event is raised and later handled."

 

This is bad, isn't it? It should be exactly the opposite. That is why, it is better to supply a parameter to Raise which you later receive in the Execute method, instead of gluing state on the event handler which can always change before you reach execute and you will execute it with the wrong data.

Message 13 of 25


@markvu94ZVF wrote:

You can work around this by wrapping the Revit event. For example, you can do the following:

 

abstract public class RevitEventWrapper<T> : IExternalEventHandler 
{
    private object @lock;
    private T savedArgs;
    private ExternalEvent revitEvent;
 
    public RevitEventWrapper() 
    {
        revitEvent = ExternalEvent.Create(this);
        @lock = new object();
    }
 
    public void Execute(UIApplication app) 
    {
        T args;
 
        lock (@lock)
        {
            args = savedArgs;
            savedArgs = default(T);
        }
 
        Execute(app, args);
    }
 
    public string GetName()
    {
        return GetType().Name;
    }
 
    public void Raise(T args)
    {
        lock (@lock)
        {
            savedArgs = args;
        }
 
        revitEvent.Raise();
    }
 
    abstract public void Execute(UIApplication app, T args);
}

 

 Then, you can implement a handler that takes arguments, like so:

public class EventHandlerWithStringArg : RevitEventWrapper<string>
{
    public override void Execute(UIApplication uiApp, string args)
    {
        // Do your processing here with "args"
    }
}

Finally, you can raise your event like this

 

EventHandlerWithStringArg myEvent = new EventHandlerWithStringArg();
.
.
.
myEvent.Raise("this is an argument");

 

There are threading pitfalls, of course, but these are outside the scope of this answer, this is the best you can do given Revit current architecture

 


That is brilliant. Imagine the generic parameter being an Action which I can simply execute:

 

        public override void Execute(UIApplication app, Action action)
        {
            action();
        }
Message 14 of 25

I don't think the example solves the issue of race condition if we are raising multiple events right after each other with different arguments. In my case, I am consuming items from a producer-consumer queue and raising the same event for every consumed item, but with different arguments.

 

            foreach (WorkItem workItem in _taskQ.GetConsumingEnumerable()) {
                ExternalApplication.addedElementRetrievalEventHandler.doc = workItem.doc;
                ExternalApplication.addedElementRetrievalEventHandler.elementId = workItem.elementId;
                ExternalApplication.addedElementRetrievalEventHandler.pointCloudElements = workItem.pointCloudElements;

                ExternalApplication.addedElementRetrievalEvent.Raise();

                // Wait for the event to complete before queueing the next one.
                // Otherwise the handler may have wrong data.
                // Event uses data in the handler.
                do {
                    Thread.Sleep(1000);
                } while (ExternalApplication.addedElementRetrievalEvent.IsPending);
            }

To ensure that the raised event gets the correct variables, which are not overridden by the next Raise() event, I wait till the raised event is no longer pending.

 

The problem is that IsPending seems to return false as soon as (or just before) the raised event starts the Execute() method. Here is an example of the Execute() method:

        public void Execute(UIApplication ui_app) {
            Thread.Sleep(60000);

            // Use elementId here.
            ElementId b = elementId;
        }

I have added the sleep here to reproduce the issue. Let's consider the following execution order:

 

1) Event #1 raised with ElementId = 123

2) Event #1 is pending

3) Event #1 is no pending

4) Event #1 starts execution

5) Event #1 sleeps for 60 seconds.

6) Event #2 is raised with ElementId = 456

7) Event #2 is pending

😎 Event #1 sleep is over and copies elementId to variable "b".

 

In the above case, b is 456 instead of 123.

 

I don't see how the method using lock mentioned above will avoid this problem. If I am missing something or if you have a workaround, please let me know.

 

Message 15 of 25

Initiate a new DocumentChanged event at the end of Event 1 just for triggering Event 2.

Message 16 of 25

Interesting. Can you explain in a little more detail why and how initiating a DocumentChanged event will help?

Message 17 of 25

Question is Event 2 and Event 1 raised from the same or different transactions?

Message 18 of 25

Both events are raised from the same worker thread and are outside of any transaction, since I am only reading data and not modifying the document.

Message 19 of 25

Use a Properties Settings bool defaulted to False. Event 1 sets it to 'True. Event 2 checks bool is 'True' before firing and when it does sets to back to 'False'.

Message 20 of 25

Good idea. Thanks!

Can't find what you're looking for? Ask the community or share your knowledge.

Post to forums  

Autodesk DevCon in Munich May 28-29th


Rail Community