Revit classes and WPF? The first M in MVVM

I have been receiving inquiries on my blog regarding the use of Revit classes inside the WPF window, because I mentioned that we must not use the Revit API classes inside our view models, the reason being the Revit classes obstruct the use of view-view model functionality during the design of our WPF windows. So how are we going to use these functions/classes especially in Revit’s Document class when the WPF window is active?

This is now the turn of the first M in the acronym MVVM, which is the model.

We represent some of our Revit classes in a model that we will call here the Revit model.

Inside this Revit model, the UIApplication, UIDocument, and Document classes are represented by their respective variables, which are made private. These variables shall be initialized inside the model’s constructor, which has an argument of a UIApplication class. We name this modelRevitBridge.cs.

Below is a sample of it.

    class modelRevitBridge
    {
        // Just like what you do when creating a Revit command, declare the necessary variable such as below.
        private UIApplication UIAPP = null;
        private Application APP = null;
        private UIDocument UIDOC = null;
        private Document DOC = null;

        // The model constructor. Include a UIApplication argument and do all the assignments here.
        public modelRevitBridge(UIApplication uiapp)
        {
            UIAPP = uiapp;
            APP = UIAPP.Application;
            UIDOC = UIAPP.ActiveUIDocument;
            DOC = UIDOC.Document;
        }

        // This function will be called by the Action function in the view model, so it must be public.
        public List<string> GenerateParametersAndValues(int idIntegerValue)
        {
            List<string> resstr = new List();

            Element el = DOC.GetElement(new ElementId(idIntegerValue));
            if (el != null)
            {
                foreach (Parameter prm in el.Parameters)
                {
                    string str = prm.Definition.Name;
                    str += " : ";
                    str += prm.AsValueString();

                    resstr.Add(str);
                }
            }

            return resstr.OrderBy(x => x).ToList();
        }
    }

Below is the view model that we integrate with the view later. Name this as viewmodelRevitBridge.cs. This class has a Dictionary that holds all the wall types’ name and id in integer value. It also has a variable of type int that holds the selected value from the combo box, as well as a list of string declared as an ObservableCollection where we save all the parameter information of the selected wall type. Also, we create a pair of command variable and action function to be connected to the button that will generate the parameters in strings.

        private Dictionary<string, int> _dicWallType;
        private int _selectedWallType;
        private ObservableCollection<string> _listParameters;

        // Declare the Revit model class here.
        // Consequently, create a get-set variable representing this.
        private modelRevitBridge _revitModel;

        public Dictionary DicWallType
        {
            get
            {
                return _dicWallType;
            }

            set
            {
                SetProperty(ref _dicWallType, value);
            }
        }

        public int SelectedWallType
        {
            get
            {
                return _selectedWallType;
            }

            set
            {
                SetProperty(ref _selectedWallType, value);
            }
        }

        public ObservableCollection<string> ListParameters
        {
            get
            {
                return _listParameters;
            }

            set
            {
                SetProperty(ref _listParameters, value);
            }
        }

        //  Commands
        // This will be used by the button in the WPF window.
        public ICommand RetrieveParametersValuesCommand
        {
            get
            {
                return new DelegateCommand(RetrieveParametersValuesAction);
            }
        }

        // The get-set variable
        internal modelRevitBridge RevitModel
        {
            get
            {
                return _revitModel;
            }

            set
            {
                _revitModel = value;
            }
        }

        // The action function for RetrieveParametersValuesCommand
        private void RetrieveParametersValuesAction()
        {
            if (SelectedWallType != -1)
            {
                ListParameters = new ObservableCollection(RevitModel.GenerateParametersAndValues(SelectedWallType));
            }
        }

        // Constructor
        public viewmodelRevitBridge()
        {

        }

The following, on the other hand, is a sample of a view of our sample project, coded in XAML. Let us name this viewRevitBridge.xaml. This view will have a combo box and a list box.

Edit the corresponding xaml.cs file as well so that we make this window disposable and we hide the minimize and maximize button.

<Window x:Class="RevitBridgeSample.viewRevitBridge"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:RevitBridgeSample"
        mc:Ignorable="d"
        Title="Revit Bridge Sample" Height="456" Width="279" ResizeMode="NoResize" WindowStartupLocation="CenterScreen" ShowInTaskbar="False" SourceInitialized="Window_SourceInitialized">
    <Window.DataContext>
        <local:RevitBridgeViewModel/>
    </Window.DataContext>
    <Grid>
        <ComboBox x:Name="comboBox" Height="35" Margin="10,10,10,0" VerticalAlignment="Top" ItemsSource="{Binding DicWallType}" DisplayMemberPath="Key" SelectedValuePath="Value" SelectedValue="{Binding SelectedWallType}"/>
        <ListBox x:Name="listBox" Margin="10,95,10,77" ItemsSource="{Binding ListParameters}"/>
        <Button x:Name="bOk" Content="OK" HorizontalAlignment="Right" Height="37" Margin="0,0,93,10" VerticalAlignment="Bottom" Width="105" IsDefault="True" Click="bOk_Click"/>
        <Button x:Name="bCan" Content="Cancel" HorizontalAlignment="Right" Height="27" Margin="0,0,10,10" VerticalAlignment="Bottom" Width="78" IsCancel="True"/>
        <Button x:Name="bProp" Content="Retrieve Parameters and values" Height="30" Margin="10,50,10,0" VerticalAlignment="Top" Command="{Binding RetrieveParametersValuesCommand, Mode=OneWay}"/>
    </Grid>
</Window>

Lastly, this will be the sample code for the Revit command, which will run the WPF window.

class RevitBridgeCommand : IExternalCommand
{
   public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
   {
       UIApplication uiapp = commandData.Application;
       UIDocument uidoc = uiapp.ActiveUIDocument;
       Application app = uiapp.Application;
       Document doc = uidoc.Document;

       try
       {
           // Get all the wall types in the current project and convert them in a Dictionary.
           FilteredElementCollector felc = new FilteredElementCollector(doc).OfClass(typeof(WallType));
           Dictionary<string, int> dicwtypes = felc.Cast().ToDictionary(x => x.Name, y => y.Id.IntegerValue);
           felc.Dispose();

           // Create a view model that will be associated to the DataContext of the view.
           viewmodelRevitBridge vmod = new viewmodelRevitBridge();
           vmod.DicWallType = dicwtypes;
           vmod.SelectedWallType = dicwtypes.First().Value;

           // Create a new Revit model and assign it to the Revit model variable in the view model.
           vmod.RevitModel = new modelRevitBridge(uiapp);

           System.Diagnostics.Process proc = System.Diagnostics.Process.GetCurrentProcess();

           // Load the WPF window viewRevitbridge.
           using (viewRevitBridge view = new viewRevitBridge())
           {
               System.Windows.Interop.WindowInteropHelper helper = new System.Windows.Interop.WindowInteropHelper(view);
               helper.Owner = proc.MainWindowHandle;

               // Assign the view model to the DataContext of the view.
               view.DataContext = vmod;

               if (view.ShowDialog() != true)
               {
                   return Result.Cancelled;
               }
           }

          return Result.Succeeded;
       }
       catch (Exception ex)
       {
           message = ex.Message;
           return Result.Failed;
       }
   }
}

Running this code will turn out like this:

The image below will be the initial screen, which gets all the wall types inside your Revit project.

rmw01

When you press the “Retrieve Parameters and values” button, all the parameters and their values of the selected wall type will be listed up inside the listbox.

rmw02

You can also create a function that writes something in your Revit project, like editing parameters of renaming the wall types. In this case though, we have to wrap the writing part of the code inside a Transaction class, or else the program will fail.
When you do this, every time you execute the writing function, it registers a command in the undo-redo mechanism. So if you want to run this function multiple times and want to undo or redo these process just once, you need to wrap the WPF window loading inside a TransactionGroup and just before the end of it, use the TransactionGroup.Assimilate() function.
 

4 comments

  1. LinhNguyen

    Wonderful stuff as always; many, many thanks. It took me a while to go through all of the materials in your blog. I want to ask about changing objects inside Revit from a modeless dialog. From what I understand, I need to put my execute function in a class with IExternalEventHandler interface to implement it. So my question is how to notify Revit to execute such function and close the window after pressing the OK button on the window.
    So far I have set up my program as follow:
    _In modelRevitBridge class: I create a RaiseEvent function which has an object exEvent = ExternalEvent.Create(handler); then raise such event exEvent.Raise();
    _In viewmodel: make a OKCommand bind to the OK button on the window, the OKCommand have a OnOKCommand action. The action call a new object(handler) in a class with IExternalEventHandler, then pass this object to the RaiseEvent function in modelRevitBridge: RevitModel.RaiseEvent(handler);
    How will the RevitBridgeCommand change to notify Revit to close the window and execute the function?
    Thank you, and hope to hear from you soon.

    • umadzkun

      Hello there.
      Working with modeless dialogs will be my next topic on this blog. But first I will give you some ideas.
      I suppose you already know the difference between a modal view (like I illustrated here), and a modeless view. In Revit, however, you must inform the application that you have this modeless view that you may use in some parts or the entire Revit operation.
      So there is a need to initialize this modeless view once Revit is finished its startup process.
      One thing that comes to mind is making a class derived from the inferface IExternalApplication, because it has two default event functions, and those are OnStartup and OnShutdown.
      Initialize the modeless view in OnStartup and dispose it in OnShutdown, if the modeless view is currently in use.
      Lastly, create a function that opens this modeless view everytime you need.
      So when you do these, you do not need ModelRevitBridge to do the closing work of this modeless view
      Regarding the external events, I had done some research before and as far as I know you need to wrap the class derived from IExternalEventHandler and then replicate or override the Raise function. In my projects I had just only this functionality once so until I look back what I did before I can’t recall eveything.
      Please give me some time. But for the meantime, have a look at some forum posts regarding external events and using them with modeless windows (although I am not sure if there are articles about this with WPF and MVVM).
      Cheers!

  2. LinhNguyen

    Hi umadzkun,
    I follow your suggestion and did some digging around the forum and post also. I use the Modeless Dialog example’s approach to initialize my WPF window. But the program kept throwing exception at the ExternalCommand step, I have created a post in the autodesk forum where I posted my code: https://forums.autodesk.com/t5/revit-api-forum/initialize-wpf-window-throw-exception/td-p/8221483 Can you check this one out and see where I made my mistake?
    Thanks!!!

Leave a comment