Friday, April 29, 2011

Getting the Most Out of the Winforms PropertyGrid Control in WPF

The PropertyGrid control has been missing in WPF since it's release and sadly it's still missing! There have been various attempts by third parties to provide one and I cant speak for the commercial offerings but I find the ones in the open source space to be quite lacking and incomplete. Most projects I found on codeplex are still in beta!

I really like the api exposed by the original PropertyGrid which I have been using for years now but it's a Winforms control. This presents some problems. The main issue for me with the Winforms version of the PropertyGrid is that it's not styleable. So it means that I'll have some inconsistency in my UI and it will stick out as shabby and odd.

Below is a screenshot of the PropertyGrid bound to a simple “Person” object, that exposes a complex type Address and a vehicle collection property.


As old as it may look, it works so well and does it job so nicely! I love this control. In the end, I've decided to use this in my Wpf application regardless of the oddity it brings. Clearly, functionality is a lot more important and since beauty lies in the eyes of the beholder, it's a subjective matter. And writing a brand new native Wpf PropertyGrid control is out of the question.

Looks apart, think of the great things this control can do. You practically bind your objects to it and it will list them in a neat grid, categorized, with many builtin editors for color editing, navigating for images, browsing and editing collections using the builtin CollectionEditor etc. And if this is not enough you can write simple extensions with your own custom editors. Indeed this control is a gem!
Update 4/30/2011
It seems since we are referencing a Winforms library, in particular System.Windows.Forms, our client app is forced to reference the fatter .NET 4.0 version versus just the slimmer .NET 4.0 client profile which is the default. 
It's not such a big con, but most certainly something to consider. I've left some more instructions at the bottom of this post.
One thing in particular that I've done is to wrap it up in a Custom Wpf Control because in order to use the Winforms version in Wpf, we'll need to :
  1. Interop via WindowsFormsHost (as easy as eating cake)
  2. We'd like to bind our ViewModel to the PropertyGrid directly from the View declaratively. This will allow us to avoid tight coupling with our ViewModel.
Here's what the custom controls template looks like :

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:WpfLab2.Controls"
                   xmlns:o="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms">
    <Style TargetType="{x:Type local:WpfPropertyGrid}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:WpfPropertyGrid}">
                    <Grid>
                        <WindowsFormsHost x:Name="host">
                            <o:PropertyGrid x:Name="propertyGrid1"/>
                        </WindowsFormsHost>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Minus the namespaces and the common style declarations you'll likely do for any templating requirement, it's fairly simple. All we need is to nest the Winforms PropertyGrid inside a WindowsFormsHost.

<Grid>
    <WindowsFormsHost x:Name="host">
        <o:PropertyGrid x:Name="propertyGrid1"/>
    </WindowsFormsHost>
</Grid>

That's pretty much all of the code. Simple indeed. The Wpf Control itself consists of a single dependency property minus a small plumbing effort. Attached to this article I include a sample application containing the control, so you can get a hang of how it works. Our View itself that consumes the PropertyGrid, just comes down to a single line of code that includes the Custom control binding to a property in the ViewModel declaratively eg:

 <local:WpfPropertyGrid SelectedObject="{Binding PersonItem, Mode=TwoWay}" />

See! Now powered with such a fantastic control you can provide easy editing of objects in your application and maintain your MVVM pattern. I love it.

Make sure you also read the following resource on msdn if your new to the PropertyGrid. It provides all the basics you'll need to know to get you up and running quickly.

http://msdn.microsoft.com/en-us/library/aa302326.aspx (Basics about the PropertyGrid)
http://msdn.microsoft.com/en-us/library/ms742875.aspx (About hosting winforms in wpf)

The sample application included is a basic example using a single person object that in turn has a complex property and a collection property. Once you edit the properties and hit the Ok button, it will display the changes in the object via a messagebox.

Nothing fancy, but you can see some simple mvvm, creation of a simple custom value converter for the complex type “Address” exposed as a property and a the creation of a simple collection editor to allow editing the “Vehicles” collection property. Just enough to get you started.

Update: 4/30/2011
A gotcha I forgot to mention is that you'll have to reference System.Windows.Forms which resides in the System.Windows.Forms assembly. The PropertyGrid control resides in this dll. When trying to reference this library from the project references dialog, you won't find it in the list of available dlls. That's because by default your project is using the .Net 4.0 client profile, so go in your project properties window and change it from .Net 4.0 client profile to .Net 4.0. After this step you can try referencing the dll again and it will be in the list.

The Microsoft .NET Framework 4 Client Profile provides a subset of features from the .NET Framework 4. The Client Profile is designed to run client applications and to enable the fastest possible deployment for Windows Presentation Foundation (WPF) and Windows Forms technology. Application developers who require features that are not included in the Client Profile should target the full .NET Framework 4 instead of the Client Profile.
Update: 5/25/2011
I was able to change the target framework back to the default Client profile after referencing System.Windows.Forms ; It seems this is already in the Client profile. What will throw you off is if your referencing System.Design.dll which will require the Full version of .NET. One typical requirement will arise for you when developing custom TypeEditors because most of the existing type editors are in the System.Design dll. Still thankfully, my needs for custom type editors was pretty basic and I got away developing one from scratch ( inheriting TypeEditor).

I found that not setting height and width explicitly for the control makes its load time slow and at most awkward even. The fix is to be explicit with the height and width. This must be due to layout differences between WPF and Windows Forms. The following article on msdn has the meat and potatoes.

http://msdn.microsoft.com/en-us/library/ms744952.aspx

So, in case its not clear, when using the control, this is what you want to attempt :

<my:WpfPropertyGrid SelectedObject="{Binding PersonItem}" Width="290" Height="350">
            </my:WpfPropertyGrid>

Notice the explicit Width and Height above. Now the speed should be super fast! Keep reading the article on msdn I link to above, it has some pretty good information.

And a small correction to the article, it's not entirely true that you cannot style the property grid. You can do some basic styling of the Winforms PropertyGrid. What you cannot do is enjoy a complete designer experience like you can currently with WPF controls. So, it's not as traggic as I made it sound.

Download the sample application

8 comments:

  1. Yeah, I went down the WPG route only to have Legal balk at the patent clause in the license. So I'm switching over to this now.

    ReplyDelete
  2. Hi Alessandro,
    I have tried to use your PropertyGrid into WPF UserControl but when my app start and the binding value for PropertyGrid is null the method OnSelectedObject at line 19 give null exception because PropertyGrid is null... I don't understand where is the mistake.
    P.s (Sono anche io di Biella ;-))

    ReplyDelete
  3. Ciao Luca :D

    I have a little mistake in the code sample. In the Views codebehind : MainWindow.xaml.cs you need to change :

    public MainWindow()
    {
    InitializeComponent();
    DataContext = new MainWindowViewModel();
    }

    to :

    public MainWindow()
    {
    InitializeComponent();
    Loaded += MainWindow_Loaded;
    }

    void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
    DataContext = new MainWindowViewModel();
    }

    Otherwise the dependency property will fire too early (before the propertygrid is created and give you a null reference as you have noted).

    This is one of the mistakes I have fixed in my production code and forget to update here.

    Thanks for pointing it out. By the way, this propertygrid is simply awesome and looks so good in my application. I really love it :)

    Alessandro

    ReplyDelete
  4. Thank you for your quick response, by the way your test project work also for me withour your workaround.


    The problem (I suppose) is related to use your WPFPropertyGrid into UserControl WPF and not into Window WPF (this is my case)...

    I have noted that i never pass on method OnApplyTemplate() and necessarily the PropertyGrid is not recovered.
    Have you tried to use it into WPF UserControl??
    Thanks again

    ReplyDelete
  5. yeah, it works as is, I just recall running into a similar jam in production code because I had nested it inside yet* another control and the like or something or the other, been a while actually.

    Basically the issue is with here :



    if you note, we are binding to the SelectedObject property, this is a dependency property, which depends on the propertygrid being available. In my case, I noticed I was binding the DataContext of the parent control too* early, as you will note from my previous comment, i resolved by ensuring that the control was created. If OnApplyTemplate did not fire, it means the control has not yet been created. Your binding too early.

    The solution is to bind the container of your propertyGrid later as I did, or to somehow ensure that it binds after the propertygrid is created.

    Lastly, it seems I never added any safety checks for null, just add one in the dependency proeprty eg :

    private static void OnSelectedObject(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
    var container = d as WpfPropertyGrid;
    if (container != null)
    {
    container.PropertyGrid.SelectedObject = container.SelectedObject;
    }
    };

    Even though this will not resolve the problem of the propertygrid being null, you can certainly avoid situations in which you bind to data, but the value of the selectedObject is still null, this is quite common. Another mistake in my sample :P

    Alessandro

    ReplyDelete
  6. mm a piece of code never made it in my comment, it's not very important but for completion here it is

    Basically the issue is with here:

    <local:WpfPropertyGrid SelectedObject="{Binding PersonItem, Mode=TwoWay}" />

    ...and the rest of my rant follows

    ReplyDelete
  7. whoops more mistakes. I'm in a rush :P

    code correction :

    I meant :

    if (container.PropertyGrid != null)

    between, that's the only difference btw my production and what you see in the sample code I posted. I'm also nested inside yet another custom control ( not a usercontrol but I don't think it changes the situation).

    ReplyDelete
  8. You are right!!! The control work perfectly.
    Thank you very much.
    Luca

    ReplyDelete