ExternalEvent.Raise() should accept parameters

Anonymous

ExternalEvent.Raise() should accept parameters

Anonymous
Not applicable

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);

 

0 Likes
Reply
Accepted solutions (1)
4,439 Views
24 Replies
Replies (24)

arnostlobel
Alumni
Alumni

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
0 Likes

Anonymous
Not applicable

Thanks for the reply Arnost!

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

0 Likes

arnostlobel
Alumni
Alumni

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

Anonymous
Not applicable

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!

0 Likes

arnostlobel
Alumni
Alumni
Accepted solution

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

joshua.lumley
Advocate
Advocate

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();
0 Likes

jeremytammik
Autodesk
Autodesk

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

0 Likes

cyril
Contributor
Contributor

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.

 

Anonymous
Not applicable

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

 

joshua.lumley
Advocate
Advocate

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

0 Likes

rossen.hristov
Enthusiast
Enthusiast

"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.

0 Likes

rossen.hristov
Enthusiast
Enthusiast

@Anonymous 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();
        }
0 Likes

Anonymous
Not applicable

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

8) 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.

 

0 Likes

joshua.lumley
Advocate
Advocate

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

0 Likes

Anonymous
Not applicable

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

0 Likes

joshua.lumley
Advocate
Advocate

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

0 Likes

Anonymous
Not applicable

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.

0 Likes

joshua.lumley
Advocate
Advocate

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'.

0 Likes

Anonymous
Not applicable

Good idea. Thanks!

0 Likes