Monday, November 16, 2009

INotifyPropertyChanged and databinding in Silverlight

In the previous article, I wrote about IronPython and databinding in WPF applications. The last note was it does not work in Silverlight. Thanks to Shri Borde (IronPython/IronRuby dev lead) who updated clrtype module, the note is not true any more.

Let's create a small Silverlight app in IronPython from scratch. I use IronPython 2.6 RC2. Follow http://lists.ironpython.com/pipermail/users-ironpython.com/2009-October/011543.html to avoid bugs in IronPython 2.6 RC2.

Create a new project:

C:\IronPython-2.6\Silverlight\script\sl.bat python BindTest

Change the app.xaml to

<usercontrol x:Class="System.Windows.Controls.UserControl"
    xmlns="http://schemas.microsoft.com/client/2007"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <stackpanel x:Name="DataPanel"
        Orientation="Horizontal">
        <textblock Text="Size"/>
        <textblock Text="{Binding size}"/>
        <textbox x:Name="tbSize"
            Text="{Binding size, Mode=TwoWay}" />
        <button x:Name="Button"
            Content="Set Initial Value"></Button>
    </StackPanel>
</UserControl>

The difference comparing to WPF version is we have to specify binding mode because the default mode for TextBox in Silverlight is OneWay. And we cannot use UpdateSourceTrigger=PropertyChanged because Silverlight does not have such UpdateSourceTrigger.

Silverlight binding is limited comparing to WPF. That's why we have to create CLR properties to Silverlight be able to see them. DevHawk has a nice serie about clr types on his blog.

Creating CLR property with clrtype.py is easy. Shri described it on IronPython mailing list. Because I use my enhanced @notify_property decorator, I can write:

class ViewModel(NotifyPropertyChangedBase):
    __metaclass__ = clrtype.ClrClass
    _clrnamespace = "BindTest"
    
    def __init__(self):
        super(ViewModel, self).__init__()
        # must be string to two-way binding work
        # correctly
        self.size = '10'

    @notify_property
    @clrtype.returns(str)
    def size(self):
        return self._size

    @size.setter
    @clrtype.accepts(str)
    def size(self, value):
        self._size = value
        print 'Size changed to %r' % self.size

The NotifyPropertyChangedBase class is the same as for WPF version. The enhanced @notify_property decorator calls automatically clrtype.accepts() for getter and clrtype.returns() for setter so we do not need to call them manually for every property:

class notify_property(property):

    def __init__(self, getter):
        def newgetter(slf):
            #return None when the property does not
            # exist yet
            try:
                return getter(slf)
            except AttributeError:
                return None
        getter = clrtype.accepts()(getter)
        clrtype.propagate_attributes(getter, newgetter)
        super(notify_property, self).__init__(newgetter)

    def setter(self, setter):
        def newsetter(slf, newvalue):
            # do not change value if the new value is
            # the same, trigger PropertyChanged event
            # when value changes
            oldvalue = self.fget(slf)
            if oldvalue != newvalue:
                setter(slf, newvalue)
                slf.OnPropertyChanged(setter.__name__)
        setter = clrtype.returns()(setter)
        clrtype.propagate_attributes(setter, newsetter)
        return property(
            fget=self.fget,
            fset=newsetter,
            fdel=self.fdel,
            doc=self.__doc__)

Then App looks similarly to the WPF counterpart:

class App:
    def __init__(self):
        self._vm = ViewModel()
        self.root = Application.Current.LoadRootVisual(
                UserControl(), "app.xaml")
        self.DataPanel.DataContext = self._vm
        self.Button.Click += self.OnClick

    def OnClick(self, sender, event):
        # must be string to two-way binding work
        # correctly
        self._vm.size = '10'

    def __getattr__(self, name):
        # provides easy access to XAML elements
        # (e.g. self.Button)
        return self.root.FindName(name)

a = App()

Run Chiron with the BindTest app

C:\IronPython-2.6\Silverlight\script\sl.bat python BindTest

and check the application in the browser on http://localhost:2060/index.html.

Whatever you write into the text box appears in the label in front of the text box when the text box loses the focus. When you click the button, the value is reseted. You can also change the value from the console:

a._vm.size= '3'

Download app.xaml and app.py. You also need clrtype.py and pyevent.py (from C:\IronPython-2.6\Tutorial\pyevent.py) in the BindTest folder.

6 comments:

Michael Foord said...

Cool - I'm actually using this at work today. I'll let you know if I have any problems... :-)

Michael Foord said...

Hmmm... when I try this in Silverlight I get "Could not add reference to assembly Microsoft.Dynamic" from clrtype.py.

Also the assertion on line 68 of clrtype.py is incorrectly written (it is written as tuple that will *always* evaluate to True).

Michael Foord said...

Dammit. So I make the add reference to Microsoft.Dynamic fully qualified (version and public key).

Next I get the error:

TypeError: Cannot create instances of DynamicOperations because it has no public constructors

Michael Foord said...

Which is weird, because when I look at DynamicOperations with reflector it has a public constructor that takes a language context. This is how it is being used in clrtype.

I need to *double check* we are using IronPython 2.6 RC 2.

Michael Foord said...

Ok, we weren't using 2.6 RC2. It works now, thanks.

Lukáš Čenovský said...

@Michael: Good it works for you.