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.
Here's an example layout:
That markup creates a layout that looks like this in
Silverlight:
Or, if you're using WPF, like this:
(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.
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:
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.
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.
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.
Inside that project, I created a single ViewModel class named ExampleViewModel.
Inside the code-behind (this is a demo, after all) of the Window
(or Page), initialize the viewmodel class:
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.
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.
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 > |
Or, if you're using WPF, like this:
(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' " + "<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); } |
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; } |
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) { ... } |
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)); } } |
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); |
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
No comments :
Post a Comment