Revit 2018.3, SetValueString and External Events

I installed the latest Revit update, and want to highlight two of the numerous interesting ongoing Revit API forum discussions:

Revit 2018.3 Update

I installed the Revit 2018.3 update released yesterday, April 9, 2018, with the build numbers 20180329_1100 and 18.3.0.81 specified in Help > About. Here is the direct download link.

This update is required for Revit 2018 to work in the new BIM 360 Design platform.

Furthermore, it includes other new functionality and contains the fixes included in the previous Revit hot fixes and updates. It updates Revit 2018 itself, Collaboration for Revit 2018, and Dynamo.

For details, please check the readme and release notes.

I extracted the following API relevant items from the latter:

More information on improvements can be found in the What's New section of the Revit 2018 online help.

About Revit 2018.3

Avoid SetValueString

Returning to some pure Revit API issues, I generally recommend avoiding the use of SetValueString to control a parameter value, since I find its behaviour somewhat unpredictable.

GetValueString returns a string representing the parameter value the way Revit thinks a user would expect to see it. For instance, it might convert an element id to the associated element name. Trying to use that approach to set an element idea sounds like a really bad idea to me.

This question came up in the Revit API discussion forum thread on editing a document on DocumentSaving and DocumentPrinting:

Question: Just before printing/saving I would like to save the smallest view scale on all sheets. This is done to circumvent the "As indicated" scale text in the title block once you add a legend to a sheet. We just want to display the scale of the view, not the legend. In case of multiple views, the smallest scale.

I'm trying to modify all sheets on catching these events by overriding a shared parameter. I can read the existing value, but not set a new one.

I tried with/without a transaction with no difference. Do these events allow modifications of the document?

Answer: I checked the DocumentPrinting event documentation.

It clearly states:

This event is raised when Revit is just about to print a view or ViewSet of the document.

Handlers of this event are permitted to make modifications to any document (including the active document), except for documents that are currently in read-only mode.

Event is cancellable. To cancel it, call the 'Cancel()' method of event's argument to True. Your application is responsible for providing feedback to the user about the reason for the cancellation.

Therefore, either something is wrong with your code, or the documentation needs fixing.

Oh no, re-reading your code, I see another possible issue:

You are using SetValueString.

Are you sure that that works at all?

Please implement access to the ManageSheetScale method from an external command that you call manually yourself from the user interface just before printing or saving and ensure that this really works.

I recommend never using SetValueString.

Try to use the appropriate override of the Parameter.Set method instead.

Then you really know what you are doing.

With SetValueString, you have no idea.

Passing Data via ExternalEvent.Raise

Another interesting discussion ensued about ExternalEvent.Raise should accept parameters:

Question: I would like to pass additional custom data when raising an external event, e.g., by passing in an own object.

It would be nice to have generics involved here, for instance, having an interface like this:

  IExternalEventHandler<T>

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

  EE.Raise(T obj);

Answer: Cyril Waechter solved this issue in Python and says:

I think I did something similar in Python.

Not a perfect solution, but well enough until I find better.

See my Python HVAC article on Revit batch view renaming, regular expressions, docstrings and a GUI and full Git6Hub code sample:

Here is an 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)

Mark Vulfson added a C# implementation as well, saying:

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.

Many thanks to Cyril and Mark for these nice and powerful extension suggestions!