pyRevit, WPF, and what ground-breaking improvements does pyChilizer bring to the table
We have been working with pyRevit for a while now, not only for the creation of pyChilizer (which is part of the official pyRevit list of extensions woohoo!) but for the creation of bespoke pyRevit-based automation for many of our clients. Historically, we have been developing Revit Add-ins for almost a decade now, and a key component has always been the ability to create a clean and responsive User Interface. This is an element that we would always invest a lot of time and effort in because getting it right is often time the difference between creating a useful workflow that saves time effort and money and provides an incredible amount of value to the client, and .. something that goes straight into the garbage bin. No matter the amount of ‘smartness’ you embed in a digital product, the UI layer enables others to engage with and make use of it.
WPF for .Net
With that said, for an ecosystem of tools based on .Net, there is only one viable framework out there – WPF (Windows Presentation Foundation). This means that, for the regular Joe, producer of Revit Add-ins, WPF is a major piece of the puzzle, an element that, sooner or later, one way or another, we have to incorporate into our software development lives in order to get to the ‘next level’. This is all well and good when we are creating typical Revit Add-ins, rooted in the .Net Framework and utilizing C# as an interpreting language. However, when we jump into the realm of Python and IronPython, things become a bit more complicated. And nigh impossible, as we will demonstrate here shortly.
It will be a tall order to cover all the ins and outs of a WPF application. Parts of it are still completely foreign to me even years after extensive use of it. Nevertheless, here are some of the basic things you would expect to accomplish when using WPF (or any other front-end framework for that matter):
- Creating a visual User Interface using a set of pre-defined commonly used affordances, such as Layouting, or Controls such as Buttons, Text Boxes, Labels, Scrollbars, Dropdowns, and so on.
- Interacting with the User Interface from your business logic layer (“When I push this button, I expect this to happen”)
- Updating the User Interface based on actions or changes in the business logic (“I want to show the number of users in this Text Box”)
There are plenty more things to unpack, but the above 3 can get you up and running for 90% of your basic use cases. So, let’s see how we can get them working using pyRevit.
Creating a UI
First things first. The very basics of getting a WPF window up and running with pyRevit. Jump to the Recipe (my favorite button when cooking) right below, Ladies and Gentlemen!
- Create your
script.py
,icon.png
combo as you normally would - Now, add a file called
ui.xaml
in the same folder - Add the below boilerplate code to it
<Windowxmlns=”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:TestUI”mc:Ignorable=”d”Title=”Test”Height=”360″Width=”300″><Grid ></Grid></Window>
from pyrevit import script, forms
xaml_file = script.get_bundle_file(‘ui.xaml’)class MyWindow(forms.WPFWindow, forms.Reactive):def __init__(self):passdef setup(self):pass
window = MyWindow()ui = script.load_ui(window, ‘ui.xaml’, True, False)ui.show_dialog()
<Windowxmlns=”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:TestUI”mc:Ignorable=”d”Title=”Test”Height=”360″Width=”300″><Grid Margin=”12″ VerticalAlignment=”Center” HorizontalAlignment=”Center”><Label FontSize=”20″>Looks like we’ve made it</Label></Grid></Window>
Interacting with the UI
Now, this is where things become more interesting. Can we, straightforwardly, give commands to Revit via our newly forged UI? Sure, why not? Let’s make Revit bark!
class MyWindow(forms.WPFWindow, forms.Reactive):def __init__(self):passdef setup(self):passdef revit_bark(self, sender, args):UI.TaskDialog.Show(“Test”, “Bark-bark”)
Recap
Let’s take a step back and figure out what we did.
- We added a Button. Easy-peasy, right?
- We wrapped the Label and the Button inside a StackPanel – that’s Layouting, my friend! WPF is quite robust when it comes to Layouting. I won’t go into details about all the different types of Layouting components, which there are not many of, but compared to other UI frameworks out there, I’d say they hit the sweet spot of flexibility vs simplicity of use. Well done, WPF!
- We added a Click behavior to our button, which we ‘wired’ to the ‘code-behind‘. (‘Code-behind’ is an important concept – traditionally, the code-behind of a WPF Window/Page/Control has the same name as the original Window/Page/Control, but ends with ‘.cs’. So, if our window is called ‘ui.xaml’, the code-behind will be a file called ‘ui.xaml.cs’. Since we are using pyRevit and Python, we deviate from this. In our case, the ‘MyWindow’ class is our code-behind!)
- By clicking our Button, we will trigger the ‘revit_bark‘ method.
This is how pyRevit handles this interaction, and there is nothing difficult or complicated about it. Things are still relatively easy to follow and understand.
Updating the UI
Let’s jump into the deep now. WPF has 2 important concepts to make our lives easier when we get to the topic of ‘but I want to create a connection between my code behind and my UI layers, how do I do that!’.
DataBinding
The first concept is the one of DataBinding. DataBinding allows us to create .. well, a ‘binding’ I suppose, between a UI element and a code-behind one. What would we normally bind? A very typical use would be to bind a List of some sort, to an ItemsControl, which allows you to visualize the list of elements in a pre-defined way without knowing what the full list may contain ahead of time. And pyRevit supports that, an example to follow.
Value Converters
The second concept is that of converting the values of the bound data via the so-called Value Converters. To give an example, imagine you have a remote control that only shows “ON” or “OFF” when you press a button, but you want it to show a green light for “ON” and a red light for “OFF” instead. In WPF, a value converter is like the person who stands between the button and the light, saying, “If the button says ‘ON,’ I’ll show the green light; if it says ‘OFF,’ I’ll show the red light.” And sadly, this is where pyRevit falls short. Because of how namespaces/classes/interfaces work, we lack the mechanisms to allow us to make use of value conversion. And that sucks a big time, as we need the ability to conditionally render the appearance of certain UI elements based on different dynamic properties. We really, really do. And, the crux of this article is to show us how we can do this.
Binding + Conditional Rendering (No Value Conversion)
Let’s see how we can make that work without using Value Conversion then.
(Final code)
<Windowxmlns=”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:TestUI”mc:Ignorable=”d”Title=”Test”Height=”360″Width=”300″><Window.Resources><SolidColorBrush x:Key=”RedBrush” Color=”#fbe5d6″/><SolidColorBrush x:Key=”BlueBrush” Color=”#dae3f3″/><SolidColorBrush x:Key=”GreenBrush” Color=”#b4f3a5″/><SolidColorBrush x:Key=”YellowBrush” Color=”#fff5cd”/></Window.Resources><Grid Margin=”12″ x:Name=”mainGrid” VerticalAlignment=”Center” HorizontalAlignment=”Center”><StackPanel><Label FontSize=”20″>Colorful Items</Label><ListBox x:Name=”itemsControl” Margin=”0 20″ ItemsSource=”{Binding items}”><ListBox.ItemTemplate><DataTemplate><Border x:Name=”itemBorder” Width=”150″ Height=”30″ Margin=”5″ Background=”White” Loaded=”Border_Loaded”><TextBlock Text=”{Binding name}” VerticalAlignment=”Center” HorizontalAlignment=”Center”/></Border></DataTemplate></ListBox.ItemTemplate></ListBox></StackPanel></Grid></Window>
import clrclr.AddReference(‘System.Windows.Forms’)clr.AddReference(‘IronPython.Wpf’)clr.AddReference(“PresentationFramework”)clr.AddReference(“PresentationCore”)clr.AddReference(“WindowsBase”)from pyrevit import script, formsclass Item:def __init__(self, name, color_key):self.name = nameself.color_key = color_keyclass MyWindow(forms.WPFWindow):def __init__(self):self.items = [Item(“George”, “RedBrush”),Item(“Sara”, “BlueBrush”),Item(“Richter”, “GreenBrush”),Item(“Lara”, “YellowBrush”)]def setup(self):self.mainGrid.DataContext = selfdef items_loaded(self, sender, args):print(“ItemsControl loaded with item count:”, sender.Items.Count)def Border_Loaded(self, sender, args):border = sender # This is the loaded Borderitem = border.DataContext # Get the data context (the bound Item instance)if hasattr(item, ‘color_key’):# Access the color resource based on the color_key of the itembrush = self.FindResource(item.color_key)border.Background = brushwindow = MyWindow()ui = script.load_ui(window, ‘ui.xaml’, True, False)ui.show_dialog()
Let’s highlight the important parts here:
- We create a new class that ‘describes’ an item. An item will have a ‘name’ value and a ‘color_key’ value.
- We create a list of these items inside the initialization of our code-behind class
- We set the DataContext of our Window to our class
- This allows us to bind the items collection as the ItemSource of the ListBox that we have created
- We say what each item inside the ListBox needs to look like – it will have a Border and inside that border, we will place a TextBlock
- This is the fun part! Even though we bind the TextBlock value directly to the ‘name’ property of each item, we have not set the background values of the border just yet! (I know we could have done that, but we are demonstrating a point where we make this decision later, which allows us to apply additional logic to it, ie. use it as a Converter)
- We tell WPF to call a method called ‘Border_Loaded’, which allows us to read the items associated with each border, grab the ‘color_key’ value from each and then use that to assign a different SolidColorBrush.
- It is also important to see how the SolidColorBrushes have been defined inside the Windows.Resources and the way we can access them in the code-behind
Conclusion
We have reviewed how we can use WPF with pyRevit to set up some potent UI capabilities! In demonstrating a typical workflow, we have covered some initial steps everyone takes to scaffold a UI layer from the ground up. What’s more, we have demonstrated an innovative way to gain Converter-like abilities which are indispensable when leveraging WPF. As always, you can find a much more complex, production-ready example of the above in our open-source pyChilizer GitHub repository. We encourage you to read, understand, and reuse our code in your pursuit of coding excellence!
Leave a Reply
Want to join the discussion?
Feel free to contribute!