Wednesday, 6 October 2010

WPF and MVVM in IronPython

I’ve been getting to grips with IronPython recently, and wanted to see how easy it would be to use the MVVM pattern. What we need is a basic library of MVVM helper functions. First is a class to load an object from a XAML file.

import clr
clr.AddReference("PresentationFramework")
clr.AddReference("PresentationCore")

from System.IO import File
from System.Windows.Markup import XamlReader

class XamlLoader(object):
    def __init__(self, xamlPath):
        stream = File.OpenRead(xamlPath)
        self.Root = XamlReader.Load(stream)
        
    def __getattr__(self, item):
        """Maps values to attributes.
        Only called if there *isn't* an attribute with this name
        """
        return self.Root.FindName(item)

In addition to loading the XAML, I’ve added a helper method to make it easy to access any named items within your XAML file, just in case the MVVM approach is proving problematic and you decide to work directly with the controls.

Next we need a base class for our view models to inherit from, which implements INotifyPropertyChanged. I thought it might be tricky to inherit from .NET interfaces that contain events, but it turns out to be remarkably simple. We just inplement add_PropertyChanged and remove_PropertyChanged, and then we can raise notifications whenever we want.

from System.ComponentModel import INotifyPropertyChanged
from System.ComponentModel import PropertyChangedEventArgs

class ViewModelBase(INotifyPropertyChanged):
    def __init__(self):
        self.propertyChangedHandlers = []

    def RaisePropertyChanged(self, propertyName):
        args = PropertyChangedEventArgs(propertyName)
        for handler in self.propertyChangedHandlers:
            handler(self, args)
            
    def add_PropertyChanged(self, handler):
        self.propertyChangedHandlers.append(handler)
        
    def remove_PropertyChanged(self, handler):
        self.propertyChangedHandlers.remove(handler)

The next thing we need is a way of creating command objects. I created a very basic class that inherits from ICommand and allows us to run our own function when Execute is called. Obviously it could easily be enhanced to properly support CanExecuteChanged and command parameters.

from System.Windows.Input import ICommand

class Command(ICommand):
    def __init__(self, execute):
        self.execute = execute
    
    def Execute(self, parameter):
        self.execute()
        
    def add_CanExecuteChanged(self, handler):
        pass
    
    def remove_CanExecuteChanged(self, handler):
        pass

    def CanExecute(self, parameter):
        return True

And now we are ready to create our application. Here’s some basic XAML. I’ve only named the grid to demonstrate accessing members directly, but it obviously is not good MVVM practice.

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="IronPython MVVM Demo"
    Width="450"
    SizeToContent="Height">
    <Grid Margin="15" x:Name="grid1">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Label Grid.Row="0" Grid.Column="0" FontSize="24" Content="First Name:" />
        <Label Grid.Row="0" Grid.Column="1" FontSize="24" Content="{Binding FirstName}" />
        <Label Grid.Row="1" Grid.Column="0" FontSize="24" Content="Surname:" />
        <Label Grid.Row="1" Grid.Column="1" FontSize="24" Content="{Binding Surname}" />
        <Button Grid.Row="2" FontSize="24" Content="Change" Command="{Binding ChangeCommand}" />
    </Grid>
</Window>

Now we can make our ViewModel. It will have FirstName and Surname attributes as well as an instance of our Command object:

class ViewModel(ViewModelBase):
    def __init__(self):
        ViewModelBase.__init__(self)
        self.FirstName = "Joe"
        self.Surname = "Smith"
        self.ChangeCommand = Command(self.change)
    
    def change(self):
        self.FirstName = "Dave"
        self.Surname = "Brown"
        self.RaisePropertyChanged("FirstName")
        self.RaisePropertyChanged("Surname")

And finally we are ready to create our application. Simply load the XAML in with the XamlLoader and set the DataContext. I also demonstrate setting the background colour here, to show how easy it is to access named elements in the XAML:

from System.Windows import Application
from System.Windows.Media import Brushes

xaml = XamlLoader('WpfMvvmDemo.xaml')
xaml.Root.DataContext = ViewModel()
xaml.grid1.Background = Brushes.DarkSalmon
app = Application()
app.Run(xaml.Root)
Now we run it and see:

mvvm-python-1

And click the button to see:

mvvm-python-2

And that’s all there is to it. It may even be simpler than doing MVVM from C#.

1 comment:

Unknown said...

Thanks for posting this. I am trying to learn Ironpython with very little previous rigorous programming experience, and it really helps to have code like this to take a look at (I was looking for examples of databinding, Ironpython, and MVVC).

There seems to be a typo in the xaml. Some HTML snuck in as a hyper-reference where grid1 is defined.

Removed the HTML and the code worked fine. Thanks.