This is the second part of a Silverlight 2 beta 2 tutorial. I have been a programmer for a few years, but are a newbee still to Silverlight. The post is written for my personal learning, but also to share the small challenges I've stumbled across and the solutions I've found to them. You will see some places that rather unexpected problems come up, but I've done my best to research and resolve them.
Background
We are going to create a Silverlight Control. What does that mean? Well, as you probably know, a control is a reusable part of an application, with a user interface and some built-in functionality. A button is an example, it has a visual element which leads the user to think she can push it, and it will translate the push into code execution, by firing an event. More advanced controls can be given parameters and data and which will be rendered and processed according to the underlying code logic.
In Silverlight, you are offered much more "control" over existing controls UI than you might be used to. The popular round button is a good example of this. In Winforms you would typically make a new control class, inherit a Button and override the OnPaint method which draws the control onto the screen. In Silverlight, the control comes with a UI defined in xaml which you get access to, and can modify and then store as a resource. This resource (or style) can be used with the instance of the standard control. This means that in many cases you can achieve a lot without having to create your own control. Worth keeping in mind!
What do we need?
In our data driven application, there are several places where we need to list some records and also provide a way to edit each one. We see a solution with a few standard buttons, a list and a customizable editor.
When adding or editing a record, we'd like the editor to replace the list.
The editor itself will not be a part of the control, but should be provided as xaml when the control is used.
So, why would we make a control?
There are usually two good reasons:
- We want to break down a large page into separate parts that are easier to handle and maintain.
- We want to encapsulate layout and functionality into a class because we are going to use it many times.
In Silverlight, there are mainly two ways of creating a control:
- Add a xaml file with an underlying .cs file. Define layout in xaml and code behind it as you like. This model is very similar to working with a page, and suits the first reason for creating a control pretty well.
- Create a new class, inherit a base control class and provide a default UI by creating a control template in generic.xaml. This model works well with the second reason above, and will result in a control that is more flexible.
We will go for the second option for the core control, but might chose to use the first option when it comes to use the control.
Lets go!
In Part #1 of this tutorial, we created a Control Library. I will use this in the following, but you might also do this directly in a SilverLight project. The difference will mainly be in the reference to the control library assembly. We have already created a new control named ListEditor, and have set the base class to ContentContol. After this, we added a test-project, and used the control in a page to see that it worked.
The code for the class was rather simple:
1: namespace QS.Silverlight.Control
2: {
3: public class ListEditor : ContentControl
4: {
5:
6: }
7: }
Defining the default layout
Before we do much more with the control, let's create some layout. Add a new xml to the project, using right click, Add -> New Item. Name the new file generic.xaml.
IMPORTANT: Select the file in the solution explorer, look at properties and change Build Action to Resource. This will cause the file to be included in the assembly as a resource, and our control can read its layout from it.
The new file will contain a one line xml declaration. Replace this line with this root element:
1: <ResourceDictionary
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:c="clr-namespace:QS.Silverlight.Controls"
5: >
6: </ResourceDictionary>
Notice the xmlns:c-reference to our library, so use the name of your library, and the prefix of your own choice here. This attribute is the mapping which allow us to refer to controls and properties in our own assembly.
Inside the root element, we will add a few lines to give our control a layout:
1: <Style TargetType="c:ListEditor">
2: <Setter Property="Template">
3: <Setter.Value>
4: <ControlTemplate TargetType="c:ListEditor">
5: <Grid Background="Yellow">
6: <TextBlock Text="Horay! we're controlling the control from generic.xaml"></TextBlock>
7: </Grid>
8: </ControlTemplate>
9: </Setter.Value>
10: </Setter>
11: </Style>
Note c:ListEditor being referenced in two places. Replace with your own name as needed.
You should think this should work now, but there is one thing missing. We have to add a contructor to our control class and set the DefaultStyleKey in order for the control to pick up this layout:
1: public class ListEditor : ContentControl
2: {
3: public ListEditor()
4: {
5: this.DefaultStyleKey = typeof(ListEditor);
6: }
7: }
Now, cross your fingers, recompile the solution and open your Page.xaml.
Hopefully, you see something like this:
You might notice we still have the content attribute set on our control in Page.xaml, but it seems to be totally ignored. There is a good explanation for this, but before we dwelve into that, try to set the background of the control to something else:
1: <Grid x:Name="LayoutRoot" Background="White">
2: <qs:ListEditor Content="Wow, it works!" Background="SlateBlue"></qs:ListEditor>
3: </Grid>
As you will see, nothing happens... Reasons are (almost) the same as for the Content not being used for anything; we haven't yet defined how it should be used.
(Really, the ContentControl we're subclassing doesn't have a layout which supports the Background property in the first place, but as you will see, the fix for the two problems are the same.)
Let's move back to the default control template in generic.xaml and see what we can do about it. Try this:
1: ...
2: <ControlTemplate TargetType="c:ListEditor">
3: <Grid Background="{TemplateBinding Background}">
4: <ContentControl Content="{TemplateBinding Content}"></ContentControl>
5: </Grid>
6: </ControlTemplate>
7: ...
Recompile and look at your page again. You have background color and text showing! Get the picture? Let's do something more useful and add a more complete xaml code to our default control template.
There are a few places where we want content to be provided from the usage of the control, but we leave those with just a comment for now.
Something like this will create our basic layout:
1: <ControlTemplate TargetType="c:ListEditor">
2: <Grid Background="{TemplateBinding Background}">
3: <Grid.RowDefinitions>
4: <RowDefinition Height="30"/>
5: <RowDefinition Height="*"/>
6: </Grid.RowDefinitions>
7: <Grid Margin="5">
8: <Grid.ColumnDefinitions>
9: <ColumnDefinition Width="*"/>
10: <ColumnDefinition Width="50"/>
11: <ColumnDefinition Width="50"/>
12: </Grid.ColumnDefinitions>
13: <TextBlock Text="Caption to come here"/>
14: <Button Grid.Column="1" Content="Add" x:Name="cAddButton"/>
15: <Button Grid.Column="2" Content="Edit" x:Name="cEditButton"/>
16: </Grid>
17: <ListBox x:Name="cList" Grid.Row="1" />
18: <Border x:Name="cEditorContainer" Grid.Row="1" Margin="5" CornerRadius="5" BorderBrush="Black" BorderThickness="1" Background="#99ddeeff">
19: <Grid>
20: <Grid.RowDefinitions>
21: <RowDefinition Height="*"/>
22: <RowDefinition Height="30"/>
23: </Grid.RowDefinitions>
24: <ContentControl Margin="5" Content="{TemplateBinding Content}"/>
25: <Grid Grid.Row="1" Margin="5">
26: <Grid>
27: <Grid.ColumnDefinitions>
28: <ColumnDefinition Width="*" />
29: <ColumnDefinition Width="50" />
30: <ColumnDefinition Width="50" />
31: </Grid.ColumnDefinitions>
32: <Button Content="Delete" x:Name="cDeleteButton" Width="50" HorizontalAlignment="Left" />
33: <Button Grid.Column="1" Content="Save" x:Name="cSaveButton" />
34: <Button Grid.Column="2" Content="Cancel" x:Name="cCancelButton" />
35: </Grid>
36: </Grid>
37: </Grid>
38: </Border>
39: </Grid>
40: </ControlTemplate>
Try this, recompile, and you should some more layout in your Page.xaml preview
Notice that we kept the default content control, so the the text from when we were testing the control now shows inside the editor area. The ListBox is shown in the same area, behind the editor, so we can only see the outer borders of it. A dummy TextBlock has been inserted for the caption, which we will fix later. Also, we still have no layout defined for each item in the ListBox.
Getting some help from code
It would be nice if we could control if the editor is shown in design or not. We can do this by adding a property to the control, and then set it in design time.
In order to "get to" a control within our control template, we override OnApplyTemplate and call GetTemplateChild with the name of the child control. Note that if someone are using a different template, these controls might not be found. So make sure you test for null before using them.
Our updated control class:
1: public class ListEditor : ContentControl
2: {
3: // Property value containers
4: private bool gShowEditor;
5:
6: // Child control handles
7: private FrameworkElement cEditorContainer;
8: private ListBox cList;
9:
10: // Constructor
11: public ListEditor()
12: {
13: this.DefaultStyleKey = typeof(ListEditor);
14: }
15:
16: // Determines if the editor or the list should show in our control
17: public bool ShowEditor
18: {
19: get { return gShowEditor; }
20: set { gShowEditor = value; UpdateControlLayout(); }
21: }
22:
23: // Called by the Silverlight framework, when the template is loaded.
24: // We use this method to get handles to our child controls
25: public override void OnApplyTemplate()
26: {
27: base.OnApplyTemplate();
28:
29: cEditorContainer = this.GetTemplateChild("cEditorContainer") as FrameworkElement;
30: cList = this.GetTemplateChild("cList") as ListBox;
31:
32: UpdateControlLayout();
33: }
34:
35: // Refresh the visual layout of the control
36: private void UpdateControlLayout()
37: {
38: if (cEditorContainer != null)
39: {
40: cEditorContainer.Visibility = ShowEditor ? Visibility.Visible : Visibility.Collapsed;
41: }
42:
43: if (cList != null)
44: {
45: cList.Visibility = ShowEditor ? Visibility.Collapsed : Visibility.Visible;
46: }
47: }
48: }
Switching back to the test page will show the editor is now hidden, because of the default value of false on gShowEditor. If you add an attribute for ShowEditor where the control is used in Page.xaml, you should get intellisense and be able to toggle between the list and the editor view.
Preview with list and editor, and the code below used to display the control.
1: <qs:ListEditor Content="Wow, it works!" Background="CornflowerBlue" ShowEditor="True"></qs:ListEditor>
If you like, you can right click Page.xaml and choose Open in Expression Blend. In Blend, you will see our new control, and you can set properties like gradient background colors and you should also be able to find the ShowEditor property, under the Miscellaneous category.
NOTE: This way of implementing a property that controls the layout of our control might work, but it is not very Silverlightish. The more fancy way is to implement a DependencyProperty and use a VisualState for each of the two states. This would allow us to control more fancy stuff like transitions between the two. This post will be large enough without this part here, but it might be a topic for an enhancement later. Until then, there is an excelent example in Shawn Burke's Blog.
Some more logic
If you run the test project, you'll see that our buttons are there, impressive and all, but doesn't do much. All five buttons should either show or hide the editor, and each one should also fire an event, so the consuming code can get notified about the change. (Even if some events like for Edit and Cancel might not be strictly necessary, I think it is a generally good idea to also communicate these actions)
We can get handles to the buttons like we did for the ListBox and editor, and we can also bind their click events in the OnApplyTemplate method. A private member for each button is created in the header section, and one (if not beautiful, very compact) way of doing this in OnApplyTemplate is like this:
1: // Called by the Silverlight framework, when the template is loaded.
2: // We use this method to get handles to our child controls
3: public override void OnApplyTemplate()
4: {
5: base.OnApplyTemplate();
6:
7: // Get handles for child controls
8: cEditorContainer = this.GetTemplateChild("cEditorContainer") as FrameworkElement;
9: cList = this.GetTemplateChild("cList") as ListBox;
10: cAddButton = this.GetTemplateChild("cAddButton") as Button;
11: cEditButton = this.GetTemplateChild("cEditButton") as Button;
12: cCancelButton = this.GetTemplateChild("cCancelButton") as Button;
13: cSaveButton = this.GetTemplateChild("cSaveButton") as Button;
14: cDeleteButton = this.GetTemplateChild("cDeleteButton") as Button;
15:
16: // Bind to child control events
17: if (cAddButton != null) cAddButton.Click += delegate(object o, RoutedEventArgs e) { this.ShowEditor = true; };
18: if (cEditButton != null) cEditButton.Click += delegate(object o, RoutedEventArgs e) { this.ShowEditor = true; };
19: if (cCancelButton != null) cCancelButton.Click += delegate(object o, RoutedEventArgs e) { this.ShowEditor = false; };
20: if (cSaveButton != null) cSaveButton.Click += delegate(object o, RoutedEventArgs e) { this.ShowEditor = false; };
21: if (cDeleteButton != null) cDeleteButton.Click += delegate(object o, RoutedEventArgs e) { this.ShowEditor = false; };
22:
23: // Refresh control layout
24: UpdateControlLayout();
25: }
As we start raising events with references to the object being edited, we will build this code more properly. But for now, we just want the control layout to respond to out buttons, so this will do.
Well, F5' it and enjoy your newest creation :)
Enabling content for Caption and DataItem
Let's start with the simple one, the caption. If you remember coding the default layout template for the editor, you probably noticed the ContentControl with a {TemplateBinding}. This one is very similar to what we need now, but it was bound to an already existing property, Content, which we inherited from our base control class.
So the first step is to create ourself a new content property, which we will call CaptionContent. In our class file, we add the following lines:
1: // Dependency properties
2: public static readonly DependencyProperty CaptionContentProperty = DependencyProperty.Register("CaptionContent", typeof(object), typeof(ListEditor), null);
3:
4: // The public Caption content property
5: public object CaptionContent
6: {
7: get { return (object)GetValue(CaptionContentProperty); }
8: set { SetValue(CaptionContentProperty, value); }
9: }
The next step is to place this content inside the layout of our control. Switch to generic.xaml and look for the TextBlock inside the second grid, where it says Text="Caption to come here". Replace this TextBlock tag with this line:
1: <ContentControl Content="{TemplateBinding CaptionContent}"/>
Recompile and switch to Page.xaml. You should now see the caption is gone. Don't worry, but try to set a new attribute on your control:
1: <qs:ListEditor ... CaptionContent="Silverlight Rocks!" />
Neat, huh? So, try this:
1: <qs:ListEditor Content="Wow, it works!" ShowEditor="True">
2: <qs:ListEditor.Background>
3: <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
4: <GradientStop Color="#FFED7676" Offset="0"/>
5: <GradientStop Color="#FF1C1A2D" Offset="1"/>
6: </LinearGradientBrush>
7: </qs:ListEditor.Background>
8: <qs:ListEditor.CaptionContent>
9: <Border BorderBrush="Brown" BorderThickness="1" Padding="3" Background="Bisque" CornerRadius="4" Margin="0,0,3,0">
10: <TextBlock FontSize="10" FontWeight="Bold">This is just cool!!</TextBlock>
11: </Border>
12: </qs:ListEditor.CaptionContent>
13: </qs:ListEditor>
Don't pay much attention to the Background. This came from setting a gradient background in Blend. Rather note the CaptionContent, set from an element this time instead of an attribute.
This should render a preview like this:
Well, so much for the easy part... we got a bit more trouble in sight with the ItemTemplate, but it will be just as flexible, so you can have any content presenting your items in the list.
First, let's turn off the editor, by setting ShowEditor="False" on the control. As we see, the ListBox has no items. There are more than one reason for this, but most important is we haven't given it any data to show. It would now be my preference to provide some data to the control in the code behind Page.xaml. This would work in run-time, but not in design. Currently, visual studio renders the preview only from the xaml and the compiled controls code, not using your codebehind file. To avoid having to run the project to see each change, we'll do a small trick to have some dummy data in the list.
In the constructor of our control class, we will check if there is we're running in design or not, and if we are, we will make a simple list of strings and use as our data source.
1: // Constructor
2: public ListEditor()
3: {
4: this.DefaultStyleKey = typeof(ListEditor);
5:
6: if (!System.Windows.Browser.HtmlPage.IsEnabled)
7: {
8: this.DataContext = CreateDummyDataForPreview();
9: }
10: }
11:
12: // Create some dummy data for preview.
13: private object CreateDummyDataForPreview()
14: {
15: List<string> vList = new List<string>();
16: for (int i = 0; i < 10; i++)
17: vList.Add(string.Format("Item #{0:00}", i));
18: return vList;
19: }
Now we have set the data context of our control, but we haven't said anything about where our list should get it's data from. It you recompile and look at preview, nothing should have happened yet. Go to generic.xaml, and set the ItemsSource property on the ListBox element:
1: <ListBox x:Name="cList" Grid.Row="1" ItemsSource="{Binding}" />
Recompile, switch to Page.xaml and you should see the list populated with 10 items.
Note: My first attempt on this was to set DataContext="{TemplateBinding DataContext}" and other combinations between these two, using ListSource, DataContext, TemplateBinding and Binding. Honestly, right now I don't quite see why that wouldn't work, but I am sure there is a logical explanation to it. It might have to do with that the DataContext is set in code and/or that we are binding to data and not layout elements, but unfortunately, I cannot say... Comments are most welcome :)
Now we look back to the control code to make another content property, following almost exactly what we did with the CaptionContent. This time, we call the property ItemTemplate, and let it be of type DataTemplate. This section should now look like this:
1: // Dependency properties
2: public static readonly DependencyProperty CaptionContentProperty = DependencyProperty.Register("CaptionContent", typeof(object), typeof(ListEditor), null);
3: public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(ListEditor), null);
4:
5: // The public Caption content property
6: public object CaptionContent
7: {
8: get { return (object)GetValue(CaptionContentProperty); }
9: set { SetValue(CaptionContentProperty, value); }
10: }
11:
12: // The public property for setting the template for a data item
13: public DataTemplate ItemTemplate
14: {
15: get { return (DataTemplate)GetValue(ItemTemplateProperty); }
16: set { SetValue(ItemTemplateProperty, value); }
17: }
Providing a template for the data item puzzled me for a while. I have an earlier post a few weeks back which describes this battle. What I learned is that the ItemTemplate of the ListBox must be set from code, not from the layout template.
Inside the OnApplyTemplate override, add the following line:
1: // Set the template defining each item's layout
2: if (cList != null) cList.ItemTemplate = this.ItemTemplate;
Now, if you cross your fingers, maybe throw some salt over your left shoulder while recompiling, you can go to your Page.xaml and enter a template for an item like this:
1: <qs:ListEditor.ItemTemplate>
2: <DataTemplate>
3: <Grid Height="75">
4: <Grid.ColumnDefinitions>
5: <ColumnDefinition Width="100" />
6: <ColumnDefinition Width="*" />
7: <ColumnDefinition Width="*" />
8: </Grid.ColumnDefinitions>
9: <Border Margin="10" BorderBrush="Gray" BorderThickness="1">
10: <Image Source="http://lh5.ggpht.com/roarfred/SH0BQHjDOUI/AAAAAAAAANM/9UGVcgJy5bI/phantom.png"></Image>
11: </Border>
12: <StackPanel Grid.Column="1" Margin="10">
13: <TextBlock>Name:</TextBlock>
14: <TextBlock>e-mail:</TextBlock>
15: <TextBlock>blog:</TextBlock>
16: </StackPanel>
17: <StackPanel Grid.Column="2" Margin="10">
18: <TextBlock Text="{Binding}" />
19: <TextBlock Text="{Binding}" />
20: <TextBlock Text="{Binding}" />
21: </StackPanel>
22: </Grid>
23: </DataTemplate>
24: </qs:ListEditor.ItemTemplate>
And the preview should show nicely.
Note: You might see some big gray errors in the preview during the xaml coding. Don't worry too much about these. They seemed to show when the xml is well formed, but an attribute has an invalid value. I have tried to do some try/catch-ing in OnApplyTemplate, but the error seems to be thrown outside of my reach. To get the preview back click the blue link, and with good xaml it should work again.
Binding to data
To avoid having to dig too deep into specific data sources and transports, we will make a very simple data source to bind our control to. Add a new class file named Contacts.cs to the test project and add a simple definition for a singular and a plural type:
1: public class Contact
2: {
3: public string Name { get; set; }
4: public string Blog { get; set; }
5: public string Email { get; set; }
6: public string ImageUrl { get; set; }
7: }
8:
9: public class Contacts : ObservableCollection<Contact>
10: {
11: }
We will be making an instance of the collection type and add a few contacts. Then we will set the collection as the data source for our control. In order to access our control from the codebehind of Page.xaml, we first have to give it a name, using the x:Name attribute:
1: <qs:ListEditor x:Name="cListEditor" Content="Wow, it works!" ShowEditor="False">
Now we can reach the control from code, so switch to code view (Page.xaml.cs). First, we bind the Loaded event to a method from the constructor, and create our contact list from there:
1: public Page()
2: {
3: InitializeComponent();
4: this.Loaded += new RoutedEventHandler(Page_Loaded);
5: }
6:
7: void Page_Loaded(object sender, RoutedEventArgs e)
8: {
9: Contacts vContacts = new Contacts();
10:
11: vContacts.Add(new Contact { Name = "Roar Fredriksen", Blog = "http://roarfred.blogger.com", Email = "roarfred@gmail.com", ImageUrl = "http://lh3.ggpht.com/roarfred/SH4aAJJwuCI/AAAAAAAAANU/Oq5_yt4YU2c/roar.jpg" });
12: vContacts.Add(new Contact { Name = "Scott Guthrie", Blog = "http://weblogs.asp.net/scottgu/", Email = "unknown", ImageUrl = "http://www.microsoft.com/presspass/images/gallery/execs/thumbnails/Guthrie_thumb.jpg" });
13: vContacts.Add(new Contact { Name = "Scott Hanselman", Blog = "http://www.hanselman.com/blog/", Email = "unknown", ImageUrl = "http://i.msdn.microsoft.com/aa336542.ScottHanselman(en-us,MSDN.10).jpg" });
14: vContacts.Add(new Contact { Name = "Shawn Burke", Blog = "http://blogs.msdn.com/sburke/", Email = "unknown" });
15:
16: cListEditor.DataContext = vContacts;
17: }
Hope the big guys forgive using their info and picture :)
Just to make sure thing works, try to recompile and run the project. You should see this in your browser:
The most noticable issue here is the property bindings. (We will get back to the image in a while)
If you have ever worked with databinding in .Net before, you most likely know the answer to this already. We are binding to the Contact object, and the TextBlock will want a string, so it calls .ToString() on our object, which will use the default implementation inherited from System.Object. This one returns the full name of our class. Even if it might make sense to override the ToString method, it wouldn't help us much here. We need to specify in each of the bindings, which member in the data source to bind to. The syntax for this is {Binding MemberName} which implements as:
1: <Image Source="{Binding ImageUrl}"></Image>
2: ...
3: <TextBlock Text="{Binding Name}" />
4: <TextBlock Text="{Binding Email}" />
5: <TextBlock Text="{Binding Blog}" />
Running the project now should look somewhat better:
You'll see the images are missing. I wasn't aware this would be a problem until now, the image was something I just threw in there while writing this post... A quick research trip to silverlight.net discloses two problems in our setup:
- Even if a image can have a string as a source, it cannot load an image this way from another domain
- Running in a html page from the local file system will cause problems trying to load images from other domains in general
The last point we fix by either setting up a local IIS to host this page, or by adding a website project to our solution. I don't find it necessary to go deeper into this setup here.
For the first one, we need to make a new property to our Contact class. Let it be of type BitmapImage, and name it Image:
1: private BitmapImage gImage;
2: public BitmapImage Image
3: {
4: get
5: {
6: if (!string.IsNullOrEmpty(ImageUrl))
7: {
8: if (gImage == null || gImage.UriSource.OriginalString != ImageUrl)
9: gImage = new BitmapImage(new Uri(ImageUrl));
10:
11: return gImage;
12: }
13: else
14: return null;
15: }
16: }
Then we update the binding of the Image in Page.xaml, to let it use the new property:
1: <Image Source="{Binding Image}" Stretch="UniformToFill"></Image>
The UniformToFill stretch type is set to allow for the images to enlarge just enough to fill the image control in both directions.
Recompile, run and look at the result (over an http url):
A couple of minor height/width changes was done to adjust the layout a bit.
Just for the record: I have no other relation to Scott Guthrie, Scott Hanselman or Shawn Burke than being a big fan of their work to enlighten us other on new technology. My contact with them all is limited to being a blog-reader, however an eager one.
Making the editor
This task will not be very challenging, as we only have four text strings to edit. In our Page.xaml, the control is still set up with a Content attribute, so the editor is only showing a text string. To see this, toggle the ShowEditor attribute over to True. Then just remove the Content attribute, as we will use a bit more complex XAML to define it:
1: <qs:ListEditor x:Name="cListEditor" ShowEditor="True">
Somewhere inside the element you add code to create the editor.
1: <qs:ListEditor.Content>
2: <Grid VerticalAlignment="Top">
3: <Grid.RowDefinitions>
4: <RowDefinition Height="25"/>
5: <RowDefinition Height="25"/>
6: <RowDefinition Height="25"/>
7: <RowDefinition Height="25"/>
8: </Grid.RowDefinitions>
9: <Grid.ColumnDefinitions>
10: <ColumnDefinition Width="30*" />
11: <ColumnDefinition Width="70*" />
12: </Grid.ColumnDefinitions>
13: <TextBlock>Name:</TextBlock>
14: <TextBlock Grid.Row="1">e-mail:</TextBlock>
15: <TextBlock Grid.Row="2">Blog:</TextBlock>
16: <TextBlock Grid.Row="3">Picture:</TextBlock>
17:
18: <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Name, Mode=TwoWay}" Margin="2"/>
19: <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Email, Mode=TwoWay}" Margin="2"/>
20: <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Blog, Mode=TwoWay}" Margin="2"/>
21: <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding ImageUrl, Mode=TwoWay}" Margin="2"/>
22: </Grid>
23: </qs:ListEditor.Content>
Notice how we bind each TextBox to properties of the Contact class, and that we have extended the binding with a Mode=TwoWay. This should allow for the values we edit to be written back into the object instance. In the next paragraphs, we will look how we can make the buttons work better, so the selected item is edited and events are being raised for the various actions.
Preview should show the control with the editor visible now:
Wrap it up with some code
You might suspect the editor isn't working right, and you would be absolutely right. There is no way each textbox can know which item we are editing. The good thing is however that the binding will kind of search up the control hierarchy for a data source, so we only have to set the item to be edited as DataContext for a control above the textboxes.
Before we do this, let's think about which events we'll need. As mentioned earlier, all buttons we have should fire events so the outer world is notified what is going on. Let's choose the most common way, by declaring five events in the control class, and five EventArgs classes.
At the bottom of ListEditor.cs, we add this code:
1: public class ListEditorAddEventArgs : EventArgs
2: {
3: public object NewItem { get; set; }
4: }
5: public class ListEditorEditEventArgs : EventArgs
6: {
7: public object EditedItem { get; set; }
8: }
9: public class ListEditorDeleteEventArgs : EventArgs
10: {
11: public object ItemToDelete { get; set; }
12: }
13: public class ListEditorCancelEventArgs : EventArgs
14: {
15: }
16: public class ListEditorSaveEventArgs : EventArgs
17: {
18: public object ItemToSave { get; set; }
19: }
And inside the ListEditor class, we declare the events:
1: // Public events
2: public event EventHandler<ListEditorAddEventArgs> Add;
3: public event EventHandler<ListEditorEditEventArgs> Edit;
4: public event EventHandler<ListEditorDeleteEventArgs> Delete;
5: public event EventHandler<ListEditorCancelEventArgs> Cancel;
6: public event EventHandler<ListEditorSaveEventArgs> Save;
Now it's time to extend our button events, so we should bind them to more real methods. Anonymous functions are all right for one-liners, but not for much else in my opinion. Maybe I'm just a bit old-fashioned, but I do find it to be less esthetical/more messy, at least for longer code blocks.
Inside the OnApplyTemplate method, change from the anonymous methods to named methods:
1: // Bind to child control events
2: if (cAddButton != null) cAddButton.Click += new RoutedEventHandler(cAddButton_Click);
3: if (cEditButton != null) cEditButton.Click += new RoutedEventHandler(cEditButton_Click);
4: if (cSaveButton != null) cSaveButton.Click += new RoutedEventHandler(cSaveButton_Click);
And write the handlers:
1: void cAddButton_Click(object sender, RoutedEventArgs e)
2: {
3: if (this.Add != null)
4: {
5: ListEditorAddEventArgs vArgs = new ListEditorAddEventArgs();
6: this.Add(this, vArgs);
7: if (vArgs.NewItem != null)
8: {
9: if (cEditorContainer != null)
10: {
11: cEditorContainer.DataContext = vArgs.NewItem;
12: this.ShowEditor = true;
13: }
14: }
15: }
16: }
17: void cEditButton_Click(object sender, RoutedEventArgs e)
18: {
19: if (cList != null && cList.SelectedItem != null)
20: {
21: if (this.Edit != null)
22: this.Edit(this, new ListEditorEditEventArgs { EditedItem = cList.SelectedItem });
23:
24: if (cEditorContainer != null)
25: {
26: cEditorContainer.DataContext = cList.SelectedItem;
27: this.ShowEditor = true;
28: }
29: }
30: }
31: void cSaveButton_Click(object sender, RoutedEventArgs e)
32: {
33: if (this.Save != null)
34: this.Save(this, new ListEditorSaveEventArgs { ItemToSave = cEditorContainer.DataContext });
35: this.ShowEditor = false;
36: }
I have skipped code for the delete and cancel operations, most out of pure lazyness. The functionality for those buttons should however be covered in the ones that are included.
Most of this code in the event handlers for the buttons are pretty much standard glue. But notice the Add event handler, which is used to fetch a new, initialized item. I am pretty sure this isn't standard stuff, and here as everywhere else, I am eager to hear from you about better solutions. (Requiring the client to subscribe an event is one thing, but we can really not guarantee the event is subscribed only once. If it isn't who's got the winning ticket in the lottery?)
If you run the project now, you will see that editing are working, at least kind-of-working. (Would be a good idea to turn off that ShowEditor first.)
A new problem we discover is that the items in the list doesn't change. However, if you edit an item, then another and move back and edit again on the first one, changes you made are still there. This is evidently showing the editing functionality is working, but it isn't worth much if the list doesn't show any changes.
The problem is caused by the way binding works. The bound property values will only be read automatically once. If later changes should be updates, the source object has to implement the INotifyPropertyChanged interface and fire the PropertyChanged event for a binding to reread a value. Since we did a shortcut with defining the properties in the Contact class, we have a litte bit of coding to do:
- We must implement INotifyPropertyChanged
- In every property set, we must raise the PropertyChange event
- In order to raise the event, we need code in the set block and cannot use the simple property syntax
- So, we make a private member to keep the values
Interface implementation and new property syntax for the Name property:
1: public class Contact : INotifyPropertyChanged
2: {
3: // Public events
4: public event PropertyChangedEventHandler PropertyChanged;
5:
6: // Private fields
7: private string gName;
8:
9: // Public properties
10: public string Name
11: {
12: get { return gName; }
13: set
14: {
15: gName = value;
16: if (PropertyChanged != null)
17: PropertyChanged(this, new PropertyChangedEventArgs("Name"));
18: }
19: }
20: ...
21: ...
22: }
Complete the remaining three properties in the same way. You can then run the application and you should see the data binding working as intended.
For the Add button to work, it will require some code in our page:
- We must subscribe the Add event and provide a new item
- We must subscribe the Save event and ensure the item being saved is included in our list
In other words:
1: void Page_Loaded(object sender, RoutedEventArgs e)
2: {
3: cListEditor.Add += new EventHandler<QS.Silverlight.Controls.ListEditorAddEventArgs>(cListEditor_Add);
4: cListEditor.Save += new EventHandler<QS.Silverlight.Controls.ListEditorSaveEventArgs>(cListEditor_Save);
5: ...
6: }
1: void cListEditor_Save(object sender, QS.Silverlight.Controls.ListEditorSaveEventArgs e)
2: {
3: Contacts vContacts = cListEditor.DataContext as Contacts;
4: if (vContacts != null && e.ItemToSave is Contact)
5: {
6: if (!vContacts.Contains((Contact)e.ItemToSave))
7: vContacts.Add((Contact)e.ItemToSave);
8: }
9: }
10:
11: void cListEditor_Add(object sender, QS.Silverlight.Controls.ListEditorAddEventArgs e)
12: {
13: e.NewItem = new Contact { Name = "The new guy" };
14: }
At this point, the control and our app should work with editing existing and adding new contacts.
Finishing steps
There are a number of things which can be done with this control, but I have a feeling this will do for now. Amongst the upgrades which I see most useful are:
- Layout - no futher comments should be necessary :)
- Use the VisualStateManager and make a soft transition when toggeling between the editor and the list
- Use TemplatePart attributes on the control class to allow better support for templating from visual designers
If you have gotten this far, I would really appreciate your comment on this post. Any constructive critisism or suggestion on how this could or should have been done will be most valueable to me and others.