INotifyPropertyChanged is important interface for building WPF or Silverlight applications using M-V-VM concept (MSDN article).
In simple language, you have a Model which provides access to your data (e.g in database, files, web, etc.). Then you have a ViewModel that access data in the Model via Model's interface and provides data to a View which is XAML file with UI layout. Linkage between ViewModel and View is done by binding that utilizes PropertyChanged event to properly update all UI elements.
I have found two examples how to implement INotifyPropertyChanged interface in IronPython. The first one uses __setattr__ hook. Personally, I don't like it - it is not clear and easily readable code. The second one is better because it uses properties. But you have to write self.OnPropertyChanged("my_property_name") for every property. Not ideal.
That's why I sit down and write a notify_property:
class notify_property(property): def __init__(self, getter): def newgetter(slf): try: return getter(slf) except AttributeError: return None super(notify_property, self).__init__(newgetter) def setter(self, setter): def newsetter(slf, newvalue): oldvalue = self.fget(slf) if oldvalue != newvalue: setter(slf, newvalue) slf.OnPropertyChanged(setter.__name__) return property( fget=self.fget, fset=newsetter, fdel=self.fdel, doc=self.__doc__)
With this subclass I aimed several goals:
- usage simple as @property decorator (actualy no other usage is possible as I implemented __init__ with just one parameter that must be the getter)
- when property is on yet defined, it should return None
- automaticaly handle PropertyChanged event when and only when property has changed
We also need to implement INotifyPropertyChanged interface in IronPython so we can call OnPropertyChanged method. See Overiding events in IronPython\Doc\dotnet-integration.rst to understand what means add_ and remove_ methods.
class NotifyPropertyChangedBase(INotifyPropertyChanged): PropertyChanged = None def __init__(self): self.PropertyChanged, self._propertyChangedCaller = pyevent.make_event() def add_PropertyChanged(self, value): self.PropertyChanged += value def remove_PropertyChanged(self, value): self.PropertyChanged -= value def OnPropertyChanged(self, propertyName): if self.PropertyChanged is not None: self._propertyChangedCaller(self, PropertyChangedEventArgs(propertyName))
Now we can implement a simple class with properties with change notification:
class DataObject(NotifyPropertyChangedBase): def __init__(self, size): super(DataObject, self).__init__() self.size = size @notify_property def size(self): return self._size @size.setter def size(self, value): self._size = value
You can see it is very easy - just like any other property in Python.
Finaly, let's put all together. When you run the code below, it shows a window with label, textbox and button. The label is updated as you type into the textbox and a message is written into the console as well. By default, the textbox is updated when it looses focus, so I have to change UpdateSourceTrigger to PropertyChanged. When you click the button, the value is reset. Note if you use type int instead of string the two-way bindign would not work.
import clr import System clr.AddReference('PresentationFramework') clr.AddReference('PresentationCore') from System.Windows.Markup import XamlReader from System.Windows import Application, Window from System.ComponentModel import INotifyPropertyChanged, PropertyChangedEventArgs import pyevent XAML_str = """<window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="250" Height="62"> <stackpanel x:Name="DataPanel" Orientation="Horizontal"> <label Content="Size"/> <label Content="{Binding size}"/> <textbox x:Name="tbSize" Text="{Binding size, UpdateSourceTrigger=PropertyChanged}" /> <button x:Name="Button" Content="Set Initial Value"></Button> </StackPanel> </Window>""" 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 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__) return property( fget=self.fget, fset=newsetter, fdel=self.fdel, doc=self.__doc__) class NotifyPropertyChangedBase(INotifyPropertyChanged): PropertyChanged = None def __init__(self): self.PropertyChanged, self._propertyChangedCaller = pyevent.make_event() def add_PropertyChanged(self, value): self.PropertyChanged += value def remove_PropertyChanged(self, value): self.PropertyChanged -= value def OnPropertyChanged(self, propertyName): if self.PropertyChanged is not None: self._propertyChangedCaller(self, PropertyChangedEventArgs(propertyName)) class ViewModel(NotifyPropertyChangedBase): def __init__(self): super(ViewModel, self).__init__() # must be string to two-way binding work correctly self.size = '10' @notify_property def size(self): return self._size @size.setter def size(self, value): self._size = value print 'Size changed to %r' % self.size class TestWPF(object): def __init__(self): self._vm = ViewModel() self.root = XamlReader.Parse(XAML_str) 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) tw = TestWPF() app = Application() app.Run(tw.root)
You need pyevent.py from IronPython\Tutorial\ folder to run to example.
Unfortunately, this does not work in Silverlight, probably because
the property is not .NET field. See next
atricle for Silverlight version.
7 comments:
This post is a bit old, but I thought I'd feed back an enchancement I did based on your code:
In the class NotifyPropertyChangeBase I added the following:
def declare_notifiable(self, *symbols):
for symbol in symbols:
self.define_notifiable_property(symbol)
def define_notifiable_property(self, symbol):
dnp = """
import sys
sys.path.append(__file__)
@notify_property
def {0}(self):
return self._{0}
@{0}.setter
def {0}(self, value):
self._{0} = value
""".format(symbol)
d = globals()
exec dnp.strip() in d
setattr(self.__class__, symbol, d[symbol])
With that set, I can simply call:
self.define_notifiable_property("size")
This will do the @notify_property and @eize.setter items. Not a huge win when you only have one property, but a huge win when you can say self.define_notifiable_property("size", "weight", "age", "firstName", "lastName", "address").
With that you'd get all of those properties wired up to work with WPF.
@jaman: nice enhancement.
thanks both for the super article and the enhancement as well
I just want to clarify for the very beginner how to write the new data class:
class ViewModel(NotifyPropertyChangedBase):
def __init__(self):
super(ViewModel, self).__init__()
self.declare_notifiable("size")
self.size="10"
Thank you!!!
I'm using IronPython 2.7.3 and had to correct the Xaml to have the correct case. ex: TextBox instead of textbox.
Corrected Xaml here http://pastebin.com/BxrM9G6Y
It's amazing how few GOOD resources there are for WPF with IronPython. This MVVM stuff is getting exciting!
My Ironpython version 2.7.4 and int type is working Two-Way Binding Mode.
This crashes in pyevent, even after I fix the XAML like Kenneth Gustine suggests.
Using IronPython 2.7.10.
After I type into the text box to change the size from '10' to '3', the script crashes when I tap on the button. It is reproducible.
Size changed to '10'
Size changed to '4'
Size changed to '10'
Error: Invoke() takes exactly 2 arguments (1 given)
Traceback (most recent call last):
File ".\template_notify.py", line 101, in
app.Run(tw.root)
File ".\template_notify.py", line 93, in OnClick
self._vm.size = '10'
File ".\template_notify.py", line 44, in newsetter
slf.OnPropertyChanged(setter.__name__)
File ".\template_notify.py", line 65, in OnPropertyChanged
self._propertyChangedCaller(self, PropertyChangedEventArgs(propertyName))
File "\\mosaiqdbfs\MOSAIQ_APP\RayStationScripts-temp\expandRoi\pyevent.py", line 66, in __call__
ev(args)
Great blogg you have
Post a Comment