Thursday, December 9, 2010

Lessons Learned in Personal Web Page Part 1: Dynamic XAML

Recently I undertook the effort to rewrite my personal web page for a few reasons. I always like side projects for learning so I wanted to explore some "fun" ideas such as three dimensional objects and plasma (I blogged about those here) but also some more practical ideas like dynamic content and custom list boxes. In future posts with this series I'll be covering:

  • "The Matrix Effect" from my Blog tab
  • The custom panel I used for the list box in my Projects tab
  • Fun with physics in my Twitter tab

The full source for the personal website is available on the home page, so just click the link to download. You'll also need to install the Farseer Physics Engine and the Physics Helper.

I used Jounce as the main framework for this and integrated it with the Silverlight navigation framework.

The first thing I'll cover is some of the dynamic content. I wanted to be able to edit certain entries and update them easily without re-publishing the entire XAP file. The Biography page is an example of this: the content is loaded in a separate file so I can edit and uploaded without changing the XAP.

The External File

The external file is easy: I create an XML document that contains the right namespace declarations and the content I want to show. In this case, I'm using a RichTextBox but I could use any type of root level control I wanted. Obviously, an approach like this would require some extra measures in production applications due to the security risk (you lose control over what that external content may contain).

Here is a snippet of the file I dropped into ClientBin to make it easy to access by Silverlight:

<RichTextBox xmlns="http://schemas.microsoft.com/client/2007" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  IsReadOnly="True" Background="{x:Null}">
<Paragraph FontWeight="Bold">
  Summary<LineBreak/>
</Paragraph>
 <Paragraph>
  I am a Silverlight-focused developer/architect and technical project manager with sales and entrepreneurial experience, a passion for mentoring and public speaking, 
  and a strong social media presence.
<LineBreak/>
</Paragraph>
</RichTextBox>

The View Model

The view model is responsible for fetching and displaying the text. I take away the concern of the UI rendering and just deal with the XAML as a string. Because I want to be friendly inside of the designer, I also supply some values when in design mode (the Jounce base view model provides an InDesigner bool for this).

Here is the entire view model:

namespace JeremyLiknessSL.ViewModels
{
    [ExportAsViewModel(Constants.VIEWMODEL_BIO)]
    public class BioViewModel : BaseViewModel 
    {
        private const string SAMPLE_XAML = @"<RichTextBox xmlns=""http://schemas.microsoft.com/client/2007"" xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"" "
                              +
                              @" IsReadOnly=""True"" Background=""{x:Null}""><Paragraph>This is a test text box.</Paragraph></RichTextBox>";
        private const string BIO_XAML = "Bio.xml";
                
        public BioViewModel()
        {
            if (InDesigner)
            {
                Text = SAMPLE_XAML;
            }
            else
            {                
                HelperExtensions.LoadResource(new Uri(BIO_XAML,UriKind.Relative), s => Text = s);
            }
        }

        private string _text;
        public string Text
        {
            get { return _text; }
            set
            {
                _text = value;
                RaisePropertyChanged(()=>Text);
            }
        }
    }
}

That looks simple, no? My "helper" method is designed to allow me to load any string resource from the web. In this case, I just pass a delegate that will receive the text when ready and do something with it. I silently absorb errors but of course would propagate those in a business application.

Here is the helper set of methods:

public static void LoadResource(Uri resource, Action<string> resourceLoaded)
{
    var wc = new WebClient();
    wc.DownloadStringCompleted += WcDownloadStringCompleted;
    wc.DownloadStringAsync(resource, resourceLoaded);
}

static void WcDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
    var wc = sender as WebClient;
    var callback = e.UserState as Action<string>;
    if (wc == null || callback == null)
    {
        if (callback != null)
        {
            callback(string.Empty);
        }
        return;
    }
    wc.DownloadStringCompleted -= WcDownloadStringCompleted;
    callback(e.Result);
}

Notice I wire into the call and pass the callback as part of the state. Upon the return, I get my call back refrence back and either call back with nothing if there was an error, or with the actual result if it procesed correctly.

The Dynamic XAML

Next, I need to have a control I can bind the text to and have it render an actual XAML control. I decided to create a user control which contains a Grid surface to host the control:

<UserControl x:Class="JeremyLiknessSL.Controls.DynamicXaml"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">    
    <Grid x:Name="LayoutRoot"/>
</UserControl>

In the code-behind I'll expose a dependency property to bind to. Whenever the dependency property changes, the code-behind will attempt to load that control from the XAML and inject it into the grid. The code-behind looks like this:

public partial class DynamicXaml
{
    public DynamicXaml()
    {
        InitializeComponent();
    }

    public string Xaml 
    {
        get { return GetXaml(this); }
        set { SetXaml(this, value); }
    }

    public static DependencyProperty XamlProperty =
        DependencyProperty.Register("Xaml",
                                    typeof(string),
                                    typeof(DynamicXaml),
                                    new PropertyMetadata(null, _SetXaml));

    private static void _SetXaml(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((DynamicXaml)d).LayoutRoot.Children.Clear();
        var uiElement = XamlReader.Load(e.NewValue.ToString()) as UIElement;
        if (uiElement == null) return;
        uiElement.SetValue(NameProperty, Guid.NewGuid().ToString().Replace("-", string.Empty));
        ((DynamicXaml)d).LayoutRoot.Children.Add(uiElement);
    }

    public static string GetXaml(DependencyObject obj)
    {
        return obj.GetValue(XamlProperty).ToString();
    }

    public static void SetXaml(DependencyObject obj, string value)
    {
        obj.SetValue(XamlProperty, value);
    }
}

Now, with the control in place, I can databind to my view model in the main view. The binding in the biography view looks like this:

<UserControl...>
<Controls:DynamicXaml Grid.Row="1" Xaml="{Binding Text}"/>
</UserControl>

As you can see, very straightforward. My view model also supplies a nice "default" control in design time, so I can see the result in the Visual Studio and Blend designers without having to run the application. This means we have a design-time friendly, MVVM-friendly implemenation of dynamic runtime controls. It also means I can simply edit one file when my biography needs to be updated and push it out to reflect the latest information.

Jeremy Likness

3 comments:

  1. It has always been my suspicion that Silverlight is unfit for public-facing websites. I had seen two sites. They presented horrific and painful experience. I figured they might have been badly designed and executed by lesser talents. Therefore, I reserved the final judgement until later.

    It's a good thing you've concocted up a public-facing website -- jeremylikness.com (?) -- in Silverlight. The site has borne out my suspicion about Silverlight.

    ReplyDelete
  2. I agree with you 100% - Silverlight is not for websites, it is for applications - exactly as I mention on my home page. This is a fun project to experiment with effects but definitely not a best practice for websites in general.

    ReplyDelete
  3. I don't know how i get here...
    But here you have my implementation of almost the same.

    Cheers!
    Fernando.-

    public class UiItem : ContentControl
    {
    public static readonly DependencyProperty XamlProperty =
    DependencyProperty.Register("Xaml", typeof(string), typeof(UiItem), new PropertyMetadata(null, OnXamlChanged));

    public string Xaml
    {
    get { return (string)GetValue(XamlProperty); }
    set { SetValue(XamlProperty, value); }
    }

    private static void OnXamlChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
    {
    var xaml = args.NewValue as string;
    if (string.IsNullOrEmpty(xaml))
    {
    return;
    }

    var item = (UiItem)target;

    var uiElement = XamlReader.Load(xaml) as UIElement;

    if (uiElement == null)
    {
    throw new NullReferenceException("UI Element can't be null.");
    }
    item.Content = uiElement;
    }

    ReplyDelete