Showing posts with label silverlight. Show all posts
Showing posts with label silverlight. Show all posts

Sunday, November 13, 2011

Sharing .xaml in WPF and Silverlight

Sharing code between WPF and Silverlight is not difficult. Good how-to is in the Prism guide. However sharing more complex .xaml is not so easy. You have to handle different namespaces and properties.

Handling different namespaces

Silverlight toolkit components are accessible via special namespace. So if you want to use a WrapPanel in Silverlight, you write something like this:

<UserControl x:Class="UnifiedXaml.TestControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit">
    <toolkit:WrapPanel></toolkit:WrapPanel>
</UserControl>

WPF world is easier:

<UserControl x:Class="UnifiedXaml.TestControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <WrapPanel></WrapPanel>
</UserControl>

It's clear those two .xaml files are so similar that they should be just one file. Here is one tiny line that makes it possible:

namespace UnifiedXaml
{
    public class MyWrapPanel: WrapPanel { }
}

From now on, you can share the .xaml file:

<UserControl x:Class="UnifiedXaml.TestControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:ux="clr-namespace:UnifiedXaml">
    <ux:MyWrapPanel></ux:MyWrapPanel>
</UserControl>

Handling different properties

When you follow Prism guidelines, you have separate projects for WPF and Silverlight parts. That means the WPF/SL application resources dictionaries are also separate. So the easiest way to handle different properties is to use styles. Shared .xaml file specifies just the control and its style. All necessary properties for WPF are defined in the style stored in the WPF application resources dictionary and the same is done for Silverlight.

Sunday, May 16, 2010

Distributing Silverlight application written in IronPython

When you have Silverlight application written in IronPython, it is a good idea to split it to several files so browser can cache them separately. Later, when you change something in your application, users will download only a small part. During my attemts with IronPython and Silverligt, I have found several catches. That's why I describe here my way how to distribute IronPython Silverlight application.

I distribute my application as one .html file, one .xap file, and several .zip files. I use .zip because IIS already knows what to do with .zip files. The files are:

  1. index.html
  2. app.xap
  3. IronPython.zip - contains files from IronPython-2.6.1\Silverlight\bin:
    IronPython.dll
    IronPython.Modules.dll
    
  4. Microsoft.Scripting.zip - contains files from IronPython-2.6.1\Silverlight\bin:
    Microsoft.Dynamic.dll
    Microsoft.Scripting.dll
    Microsoft.Scripting.Core.dll
    Microsoft.Scripting.ExtensionAttribute.dll
    Microsoft.Scripting.Silverlight.dll
    
  5. SLToolkit.zip - contains files form Silverlight toolkit or SDK; in our case just
  6. System.Windows.Controls.dll
    

Let's create a small application, that uses ChildWindow control from Silverlight toolkit:

C:\IronPython-2.6.1\Silverlight\script\sl.bat python childwindow
Change the app.py and app.xml:

app.py

from System.Windows import Application
from System.Windows.Controls import UserControl

class App:
    def __init__(self):
        self.root = Application.Current.LoadRootVisual(UserControl(), "app.xaml")

a = App()

app.xaml

<UserControl x:Class="System.Windows.Controls.UserControl"
  xmlns="http://schemas.microsoft.com/client/2007"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls">
  <controls:ChildWindow >
    <StackPanel>
      <TextBlock Text="Text in ChildWindow"/>
      <Button x:Name="btnNewWindow" Content="New window"/>
    </StackPanel>
  </controls:ChildWindow>
</UserControl>

We don't want to Chiron automatically add necesary .dll files into .xap so we have to add our own AppManifest.xaml and languages.config into childwindow\app folder:

AppManifest.xaml

<Deployment xmlns="http://schemas.microsoft.com/client/2007/deployment"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  RuntimeVersion="2.0.31005.0"
  EntryPointAssembly="Microsoft.Scripting.Silverlight"
  EntryPointType="Microsoft.Scripting.Silverlight.DynamicApplication"
  ExternalCallersFromCrossDomain="ScriptableOnly">
  <Deployment.Parts>
  </Deployment.Parts>
  <Deployment.ExternalParts>
    <ExtensionPart Source="Microsoft.Scripting.zip" />
    <ExtensionPart Source="SLToolkit.zip" />
  </Deployment.ExternalParts>
</Deployment>

languages.config

<Languages>
  <Language names="IronPython,Python,py"
    languageContext="IronPython.Runtime.PythonContext"
    extensions=".py"
    assemblies="IronPython.dll;IronPython.Modules.dll"
    external="IronPython.zip"/>
</Languages>

Now create all three .zip files and add them into childwindow folder.

To test the application with Chiron, run

C:\IronPython-2.6.1\Silverlight\bin\Chiron.exe /e: /d:childwindow

The /e: switch is important - it tells Chiron to not put any assembly into generated .xap file. Check the application on http://localhost:2060/index.html.

To generate .xap file for distribution, run:

C:\IronPython-2.6.1\Silverlight\bin\Chiron.exe /e: /d:childwindow\app /z:app.zap

If you want to use anything from external assemblies in the code, you have to add manually reference to those assemblies. For example, if you want to add a button that creates a new ChildWindow, you have to change you code like this:

app.py

from System.Windows import Application
from System.Windows.Controls import UserControl

class App:
    def __init__(self):
        self.root = Application.Current.LoadRootVisual(UserControl(), "app.xaml")
        self.root.btnNewWindow.Click += self.OnClick

    def OnClick(self, sender, event):
        import clr
        clr.AddReference('System.Windows.Controls')
        from System.Windows.Controls import ChildWindow
        self.root.panel.Children.Add(ChildWindow(Content='new window'))

a = App()

app.xaml

<UserControl x:Class="System.Windows.Controls.UserControl"
  xmlns="http://schemas.microsoft.com/client/2007"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls">
  <controls:ChildWindow >
    <StackPanel x:Name="panel">
      <TextBlock Text="Text in ChildWindow"/>
      <Button x:Name="btnNewWindow" Content="New window"/>
    </StackPanel>
  </controls:ChildWindow>
</UserControl>

If you comment out the clr.AddReference line, ImportError appears. See the explanation in Jimmy's email.

You can download the example here but note the .zip files do not contain and .dlls.

Wednesday, May 12, 2010

Silverlight validation with IronPython

Validation support in Silverlight is done via Visual State Manager. All invalid fields have red rectangle around themselves. Unfortunately, this does not work out of the box in IronPython. We have to push it a little bit.

To demonstrate how, I have created a small example. Create a Silverlight app template and change app.py and app.xaml:

C:\IronPython-2.6.1\Silverlight\script\sl.bat python validation
app.py
import clrtype
import pyevent
from System.Windows import Application
from System.Windows.Controls import UserControl
from System.ComponentModel import INotifyPropertyChanged, PropertyChangedEventArgs

class ValidationClass(INotifyPropertyChanged):
    __metaclass__ = clrtype.ClrClass
    PropertyChanged = None

    def __init__(self, win):
        self.win = win
        self._text = 'text'
        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))

    @property
    @clrtype.accepts()
    @clrtype.returns(str)
    def text(self):
        return self._text

    @text.setter
    @clrtype.accepts(str)
    @clrtype.returns()
    def text(self, value):
        if not value.startswith('text'):
            raise Exception('Value must start with text!')
        self._text = value
        self.OnPropertyChanged('text')

class App:
    def __init__(self):
        self.root = Application.Current.LoadRootVisual(UserControl(), "app.xaml")
        self.root.DataContext = ValidationClass(self.root)

App()
app.xaml
<UserControl x:Class="System.Windows.Controls.UserControl"
  xmlns="http://schemas.microsoft.com/client/2007"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <StackPanel>
    <TextBox x:Name="tbValidate1" Width="100" Height="25" 
      Text="{Binding text, Mode=TwoWay, ValidatesOnExceptions=True,
      NotifyOnValidationError=True}" />
    <TextBox Width="100" Height="25" />
    <TextBlock Text="{Binding text}" HorizontalAlignment="Center" />
  </StackPanel>
</UserControl>

When you run this application (C:\IronPython-2.6.1\Silverlight\script\server.bat /d:validation), you'll find out the validation does not work. There is no red rectangle when you enter wrong value; e.g. wrong.

Note the second empty TextBox is there so you can move focus out of the first one to update bound property.

For whatever reason, the invalid component is not switched into invalid state. Could be IronPython bug, could be something else. Anyway to fix it, you have to switch the control into invalid state manually. Add the BindingValidationError event:

from System.Windows import VisualStateManager
from System.Windows.Controls import ValidationErrorEventAction

...

class App:
    def __init__(self):
        self.root = Application.Current.LoadRootVisual(UserControl(), "app.xaml")
        self.root.DataContext = ValidationClass(self.root)
        self.root.BindingValidationError += self.OnBindingValidationError

    def OnBindingValidationError(self, sender, event):
        if event.Action == ValidationErrorEventAction.Added:
            VisualStateManager.GoToState(event.OriginalSource, 'InvalidUnfocused', True)
        else:
            VisualStateManager.GoToState(event.OriginalSource, 'Valid', True

Now when you enter wrong value into TextBox, you can see red rectangle around the control. You also see, the bound variable has the old, correct value text:

You can download the whole source here.

Tuesday, March 16, 2010

Parsing XML with XDocument

I needed to parse a XML document recently in Silverlight. Unfortunately, Silverlight does not have System.Xml.XmlDocument type so you need to use System.Xml.Linq.XDocument.

The following example works in Silverlight and with small change also in WPF.

# encoding: utf-8
import clr
clr.AddReferenceToFile('System.Xml.Linq.dll')
from System.Xml.Linq import XDocument, XNamespace

content = """<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss"
    xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <item>
            <title>This is the title</title>
            <media:description type="html"><p></p></media:description>
            <link>html/dsc00001.html</link>
            <media:thumbnail url="preview/dsc00001.jpg"/>
            <media:content url="web/dsc00001.jpg"/>
        </item>
        <item>
            <title></title>
            <media:description type="html"><p></p></media:description>
            <link>html/dsc00002.html</link>
            <media:thumbnail url="preview/dsc00002.jpg"/>
            <media:content url="web/dsc00002.jpg"/>
        </item>
    </channel>
</rss>"""

xDoc = XDocument().Parse(content)
namespace = XNamespace.Get("http://search.yahoo.com/mrss")
for item in xDoc.Element('rss').Element('channel').Elements('item'):
    print item.Element('title').Value
    print item.Element(namespace+'thumbnail').Attribute('url').Value
Here is the output:
This is the title
preview/dsc00001.jpg

preview/dsc00002.jpg

You have to have System.Xml.Linq.dll from Silverlight SDK next to your app.py.

The change for WPF:

clr.AddReference('System.Xml.Linq')

Also make sure you don't have Silverlight's System.Xml.Linq.dll next to your script.

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.