Thursday, 2 August 2012

Dynamically Generating Controls in WPF and Silverlight

Some of the Windows Forms developers I've spoken to have said that one thing they want to learn is how to dynamically create controls in WPF and Silverlight. In this post I'll show you several different ways to create controls at runtime using Silverlight 4 and WPF 4.
First, we'll start with how to create controls in XAML. From there, we'll move to dynamically-loaded XAML before we take a look at using the CLR object equivalents.

Creating Controls at Design Time in XAML

Creating controls using the design surface and/or XAML editor is definitely the easiest way to create your UI. You can use Expression Blend or Visual Studio, depending upon how creative you want to be. If you want a more dynamic layout, you can hide and show panels at runtime.

Here's an example layout:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<Grid Margin="10">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="100" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
             
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
 
    <TextBlock Text="First Name"
                Height="19"
                Margin="0,7,31,4" />          
    <TextBox x:Name="FirstName"
                Margin="3"
                Grid.Row="0"
                Grid.Column="1" />
             
    <TextBlock Text="Last Name"
                Margin="0,7,6,3"
                Grid.Row="1"
                Height="20" />
    <TextBox x:Name="LastName"
                Margin="3"
                Grid.Row="1"
                Grid.Column="1" />
 
 
    <TextBlock Text="Date of Birth"
                Grid.Row="2"
                Margin="0,9,0,0"
                Height="21" />
    <sdk:DatePicker x:Name="DateOfBirth"
                    Margin="3"
                    Grid.Row="2"
                    Grid.Column="1" />
 
 
    <Button x:Name="SubmitChanges"
            Grid.Row="3"
            Grid.Column="3"
            HorizontalAlignment="Right"
            VerticalAlignment="Top"
            Margin="3"
            Width="80"
            Height="25"
            Content="Save" />
</Grid>
That markup creates a layout that looks like this in Silverlight:

image
Or, if you're using WPF, like this:
image
(note that you'll need to remove or remap the "sdk" prefix when using this XAML in WPF, as the date control is built-in)
Once you're familiar with working in XAML, you can easily modify the process to load the XAML at runtime to dynamically create controls.

Creating Controls at runtime using XAML strings

In Silverlight, this block of code in the code-behind creates the same controls at runtime by loading the XAML from a string using the System.Windows.Markup.XamlReader class. This class exposes a Load method which (in Silverlight) takes a well-formed and valid XAML string and returns back a visual tree
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public MainPage()
{
    InitializeComponent();
 
    Loaded += new RoutedEventHandler(MainPage_Loaded);
}
 
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
    CreateControls();
}
 
 
private void CreateControls()
{
    string xaml =
    "<Grid Margin='10' " +
        "xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' " +
        "<Grid.ColumnDefinitions>" +
            "<ColumnDefinition Width='100' />" +
            "<ColumnDefinition Width='*' />" +
        "</Grid.ColumnDefinitions>" +
 
        "<Grid.RowDefinitions>" +
            "<RowDefinition Height='Auto' />" +
            "<RowDefinition Height='Auto' />" +
            "<RowDefinition Height='Auto' />" +
            "<RowDefinition Height='*' />" +
        "</Grid.RowDefinitions>" +
 
        "<TextBlock Text='First Name' Height='19' Margin='0,7,31,4' />" +
        "<TextBox x:Name='FirstName' Margin='3' Grid.Row='0' Grid.Column='1' />" +
 
        "<TextBlock Text='Last Name' Margin='0,7,6,3' Grid.Row='1' Height='20' />" +
        "<TextBox x:Name='LastName' Margin='3' Grid.Row='1' Grid.Column='1' />" +
 
        "<TextBlock Text='Date of Birth' Grid.Row='2' Margin='0,9,0,0' Height='21' />" +
        "<sdk:DatePicker x:Name='DateOfBirth' Margin='3' Grid.Row='2' Grid.Column='1' />" +
 
        "<Button x:Name='SubmitChanges' Grid.Row='3' Grid.Column='3' " +
            "HorizontalAlignment='Right' VerticalAlignment='Top' " +
            "Margin='3' Width='80' Height='25' Content='Save' />" +
    "</Grid>";
 
 
    UIElement tree = (UIElement)XamlReader.Load(xaml);
 
    LayoutRoot.Children.Add(tree);
}
Note that I needed to add the namespace definitions directly in this XAML. A chunk of XAML loaded via XamlReader.Load must be completely self-contained and syntactically correct.
The WPF XamlReader.Load call is slightly different as it has no overload which would take a string. Instead, it takes an XmlReader as one form of parameter:
?
1
2
3
4
5
6
StringReader stringReader = new StringReader(xaml);
XmlReader xmlReader = XmlReader.Create(stringReader);
 
UIElement tree = (UIElement)XamlReader.Load(xmlReader);
 
LayoutRoot.Children.Add(tree);

This technique also works for loading chunks of XAML from a file on the local machine, or as the result of a database query. It's also helpful for enabling the use of constants (like the extended color set) that are recognized by XAML parser in Silverlight, but not from code.
The more typical approach to dynamically creating controls, however, is to simply use the CLR objects.

Creating Controls at runtime using Code and CLR Objects

Everything you do in XAML can also be done from code. XAML is a representation of CLR objects, rather than a markup language that abstracts the underlying objects. Creating controls from code tends to be more verbose than doing the same from XAML. However, it is a familiar approach for Windows Forms developers, and a great way to handle dynamic UI.
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
private void CreateControlsUsingObjects()
{
    // <Grid Margin="10">
    Grid rootGrid = new Grid();
    rootGrid.Margin = new Thickness(10.0);
             
    // <Grid.ColumnDefinitions>
    //   <ColumnDefinition Width="100" />
    //   <ColumnDefinition Width="*" />
    //</Grid.ColumnDefinitions>
 
    rootGrid.ColumnDefinitions.Add(
        new ColumnDefinition() { Width = new GridLength(100.0) });
    rootGrid.ColumnDefinitions.Add(
        new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) });
 
    //<Grid.RowDefinitions>
    //  <RowDefinition Height="Auto" />
    //  <RowDefinition Height="Auto" />
    //  <RowDefinition Height="Auto" />
    //  <RowDefinition Height="*" />
    //</Grid.RowDefinitions>
 
    rootGrid.RowDefinitions.Add(
        new RowDefinition() { Height = GridLength.Auto });
    rootGrid.RowDefinitions.Add(
        new RowDefinition() { Height = GridLength.Auto });
    rootGrid.RowDefinitions.Add(
        new RowDefinition() { Height = GridLength.Auto });
    rootGrid.RowDefinitions.Add(
        new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) });
 
    //<TextBlock Text="First Name"
    //           Height="19"
    //           Margin="0,7,31,4" />
 
    var firstNameLabel = CreateTextBlock("First Name", 19, new Thickness(0, 7, 31, 4), 0, 0);
    rootGrid.Children.Add(firstNameLabel);
 
    //<TextBox x:Name="FirstName"
    //         Margin="3"
    //         Grid.Row="0"
    //         Grid.Column="1" />
 
    var firstNameField = CreateTextBox(new Thickness(3), 0, 1);
    rootGrid.Children.Add(firstNameField);
 
    //<TextBlock Text="Last Name"
    //           Margin="0,7,6,3"
    //           Grid.Row="1"
    //           Height="20" />
 
    var lastNameLabel = CreateTextBlock("Last Name", 20, new Thickness(0, 7, 6, 3), 1, 0);
    rootGrid.Children.Add(lastNameLabel);
 
             
    //<TextBox x:Name="LastName"
    //         Margin="3"
    //         Grid.Row="1"
    //         Grid.Column="1" />
 
    var lastNameField = CreateTextBox(new Thickness(3), 1, 1);
    rootGrid.Children.Add(lastNameField);
 
 
    //<TextBlock Text="Date of Birth"
    //           Grid.Row="2"
    //           Margin="0,9,0,0"
    //           Height="21" />
 
    var dobLabel = CreateTextBlock("Date of Birth", 21, new Thickness(0, 9, 0, 0), 2, 0);
    rootGrid.Children.Add(dobLabel);
 
    //<DatePicker x:Name="DateOfBirth"
    //            Margin="3"
    //            Grid.Row="2"
    //            Grid.Column="1" />
 
    DatePicker picker = new DatePicker();
    picker.Margin = new Thickness(3);
    Grid.SetRow(picker, 2);
    Grid.SetColumn(picker, 1);
    rootGrid.Children.Add(picker);
 
    //<Button x:Name="SubmitChanges"
    //        Grid.Row="3"
    //        Grid.Column="3"
    //        HorizontalAlignment="Right"
    //        VerticalAlignment="Top"
    //        Margin="3"
    //        Width="80"
    //        Height="25"
    //        Content="Save" />
 
    Button button = new Button();
    button.HorizontalAlignment = HorizontalAlignment.Right;
    button.VerticalAlignment = VerticalAlignment.Top;
    button.Margin = new Thickness(3);
    button.Width = 80;
    button.Height = 25;
    button.Content = "Save";
    Grid.SetRow(button, 3);
    Grid.SetColumn(button, 1);
    rootGrid.Children.Add(button);
 
    LayoutRoot.Children.Add(rootGrid);
}
 
private TextBlock CreateTextBlock(string text, double height, Thickness margin, int row, int column)
{
    TextBlock tb = new TextBlock()
        { Text = text, Height = height, Margin = margin };
    Grid.SetColumn(tb, column);
    Grid.SetRow(tb, row);
 
    return tb;
}
 
private TextBox CreateTextBox(Thickness margin, int row, int column)
{
    TextBox tb = new TextBox() { Margin = margin };
    Grid.SetColumn(tb, column);
    Grid.SetRow(tb, row);
 
    return tb;
}
You'll see the code is only slightly more verbose when expanded out. The two helper functions help minimize that. In the code, I create the entire branch of the visual tree before I add it to the root. Doing this helps minimize layout cycles you'd otherwise have if you added each item individually to the root.
I tend to put any UI interaction inside the Loaded event. However, you could place this same code inside the constructor, after the InitializeComponent call. As your code gets more complex, and relies on other UI elements to be initialized and loaded, you'll want to be smart about which function you use.

Handling Events

If you want to handle events, like button clicks, you'd do that like any other .NET event handler:
?
1
2
3
4
5
6
7
8
9
10
11
12
{
    Button button = new Button();
    ...
    button.Click += new RoutedEventHandler(button_Click);
 
    LayoutRoot.Children.Add(rootGrid);
}
 
void button_Click(object sender, RoutedEventArgs e)
{
    ...
}
Creating controls from code doesn't mean you lose the valuable ability to data bind. In some cases, especially where the binding source is hard to reference from XAML, binding is easier in code.

Binding Dynamically Created Controls

We haven't used any binding yet, so we'll need to create a binding source. For that, I created a simple shared project that targets Silverlight 4. It's a Silverlight class library project and is used by both the WPF and Silverlight examples. Remember, to use it from WPF 4 (without any additions), you'll need to use a file reference to the compiled DLL, not a project reference.
Inside that project, I created a single ViewModel class named ExampleViewModel.
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ExampleViewModel : INotifyPropertyChanged
{
    private string _lastName;
    public string LastName
    {
        get { return _lastName; }
        set { _lastName = value; NotifyPropertyChanged("LastName"); }
    }
 
    private string _firstName;
    public string FirstName
    {
        get { return _firstName; }
        set { _firstName = value; NotifyPropertyChanged("FirstName"); }
    }
 
    private DateTime _dateOfBirth;
    public DateTime DateOfBirth
    {
        get { return _dateOfBirth; }
        set { _dateOfBirth = value; NotifyPropertyChanged("DateOfBirth"); }
    }
 
    public event PropertyChangedEventHandler PropertyChanged;
 
    protected void NotifyPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}
Inside the code-behind (this is a demo, after all) of the Window (or Page), initialize the viewmodel class:
?
1
2
3
4
5
6
7
8
9
10
11
12
private ExampleViewModel _vm = new ExampleViewModel();
 
public MainWindow()
{
    _vm.LastName = "Brown";
    _vm.FirstName = "Pete";
    _vm.DateOfBirth = DateTime.Parse("Jan 1, 1910");
 
    InitializeComponent();
 
    ...
}

Once that is done, we can create an example binding. I'm going to use the First Name TextBox and set up two-way binding with the FirstName property of the ExampleViewModel instance.

?
1
2
3
4
5
6
7
8
var firstNameField = CreateTextBox(new Thickness(3), 0, 1);
Binding firstNameBinding = new Binding();
firstNameBinding.Source = _vm;
firstNameBinding.Path = new PropertyPath("FirstName");
firstNameBinding.Mode = BindingMode.TwoWay;
firstNameField.SetBinding(TextBox.TextProperty, firstNameBinding);         
 
rootGrid.Children.Add(firstNameField);
The same approach to expressing binding also works in XAML. It's just that we have a binding markup extension that makes the process easier.
One thing that tripped me up in this example was I passed in TextBlock.TextProperty to the SetBinding call. That's a valid dependency property, so it compiles just fine. In WPF, that fails silently, even when you have verbose binding debugging turned on. In Silverlight, it throws a catastrophic error (without any additional information). That catastrophic error made me look more closely at the call, ultimately leading to the fix.
To bind controls added using dynamically-loaded XAML, you'll need to provide a valid Name to each control you want to reference, then use FindName after loading to get a reference to the control. From there, you can using the Binding object and SetBinding method. Of course, you can also embed the binding statements directly in the XAML if you wish to do a little string manipulation.

Summary

So, we've seen that there are three different ways you can display controls in Silverlight and WPF.
  • Use the design surface / XAML Editor / Blend and create them prior to compiling
  • Load XAML at runtime
  • Use CLR Objects at runtime
Each way is useful in different scenarios, and also has different performance characteristics. XAML parsing is surprisingly efficient and the XAML can be stored in a large text field in a single row in a database or as a loose file on the file system, for example.



Source Code and Related Media

Download /media/73013/petebrown.dynamiccontrols.zip

No comments :