Tuesday, July 22, 2008

typeof <T>?

Looking for a better way to handle data insertion and updates in my Silverlight control lead me through a nice evening with a few new discoveries:

1) sladapter at silverlight.net suggested using Lutz Roeder's .Net Reflector to investigate the source code of the Silverlight DataGrid control. From barely knowing this was possible, to actually getting something useful turned out to be a one MB download away. Check out Lutz Roeder's blog, but be careful not to spend your whole day playing Digger (Boulder Dash in Silverlight)!

(Unfortunately, from what I found, the DataGrid turnes out not to support adding or deleting records, which I probably would have found easier by trying the control in an application instead of reading source code)

2) Again from tip by sladapter: There is an IEditableObject interface which can be used in Silverlight, to implement support for BeginEdit, CancelEdit and EndEdit functionality on your own data objects. New to me was also that the classes generated from a WCF was declared as partial, so I could easily add a new partial portion to implement this part.

3) I was happy to find IEditableObject, but even more happy to find IBindingList by myself. This could solve more of my problems, like additions and deletions! Only that a few minutes later I discovered this interface isn't included in Silverlight :(

4) Knowing I had to solve this by myself, I tried to look for a way to solve this problem:

If you have a collection instance, how can you create a new instance of the type of item held by the collection?

Say we got an object of ObservableCollection<Contact>. The result should then be a new Contact. (Without us knowing about the Contact class)

(IBindingList includes a method AddNew which would let us allow to push this down to the datasource. We could make our own similar interface, but that would limit our control to only work with our own data objects)

Based on a few assumptions, like that the list will be either an array type or somewhere down the road implement IEnumerable<T>, I came to the following code: (The collection object is here in this.DataContext)

   1: private object TryCreateItem()
   2: {
   3:     if (this.DataContext != null)
   4:     {
   5:         Type vCollectionType = this.DataContext.GetType();
   6:         
   7:         // If this is just an array or similar, see if we can get the element type cheap
   8:         var vElementType = vCollectionType.GetElementType();
   9:  
  10:         if (vElementType == null)
  11:         {
  12:             // See if we're a IEnumerable<T>, and if so, get the T
  13:             vElementType = GetEnumerableOfTElementType(vCollectionType);
  14:         }
  15:  
  16:         if (vElementType != null)
  17:         {
  18:             // We got the type, so see if we have a nullary (default) contructor
  19:             var vConstructor = vElementType.GetConstructor(Type.EmptyTypes);
  20:             if (vConstructor != null)
  21:             {
  22:                 // gotcha!
  23:                 object vNewItem = vConstructor.Invoke(null);
  24:                 return vNewItem;
  25:             }
  26:         }
  27:     }
  28:  
  29:     // Time for some improvements?
  30:     return null;
  31: }
  32:  
  33: private Type GetEnumerableOfTElementType(Type pCollectionType)
  34: {
  35:     // Check all interfaces implemented or inherited by this type
  36:     foreach (Type vInterface in pCollectionType.GetInterfaces())
  37:     {
  38:         // If we're a generic and the generic is IEnumerable<T>, we got it!
  39:         if (vInterface.IsGenericType && vInterface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
  40:             return vInterface.GetGenericArguments()[0];
  41:     }
  42:     
  43:     // Bad luck!
  44:     return null;
  45: }

Thursday, July 17, 2008

Part #2: Creating the Control

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.

Draft - List view

When adding or editing a record, we'd like the editor to replace the list.

Draft - Edit view

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:

  1. We want to break down a large page into separate parts that are easier to handle and maintain.
  2. 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:

  1. 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.
  2. 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.

image

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:

image

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

image

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.

image image

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:

image

 

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.

image

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:

image

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:

image

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:

  1. Even if a image can have a string as a source, it cannot load an image this way from another domain
  2. 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):

image

 

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:

image

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.

image image

image

 

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.

Sunday, July 13, 2008

Testing Windows Live Writer

I just heard of and downloaded Windows Live Writer... Now I have to try it out and see if it saves me some time over hand-coding HTML for my blog posts.

What is really a mess to get working correctly is the code formatting. From Tim Heuer's blog, I read he was using the Code Cnippet Plugin for Windows Live Writer. Hmm.. so, you geeks are not hand-coding afterall, huh? well, technology is here to help us isn't it?

Let's try to add some code in here:

   1: using System;
   2: using System.Net;
   3: using System.Windows;
   4: using System.Windows.Controls;
   5: using System.Windows.Documents;
   6: using System.Windows.Ink;
   7: using System.Windows.Input;
   8: using System.Windows.Media;
   9: using System.Windows.Media.Animation;
  10: using System.Windows.Shapes;
  11:  
  12: namespace QS.Silverlight.Controls
  13: {
  14:     public class ListEditor : ContentControl
  15:     {
  16:  
  17:     }
  18: }

Looks pretty cool this far... what about some XAML?

   1: <UserControl x:Class="SLControlTest.Page"
   2:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
   4:     Width="400" Height="300"
   5:     xmlns:qs="clr-namespace:QS.Silverlight.Controls;assembly=QS.Silverlight.Controls">
   6:     <Grid x:Name="LayoutRoot" Background="White">
   7:         <qs:ListEditor Content="Wow, it works!" Background="Red"></qs:ListEditor>
   8:     </Grid>
   9: </UserControl>

Not bad :P Will we survive some SQL too?

   1: use AspnetDB
   2:  
   3: select t.name as TableName, c.name as ColumnName, t.name as DataType, c.max_length as Length
   4: from sys.tables t
   5:     join sys.columns c on c.object_id = t.object_id
   6:     join sys.types ty on ty.user_type_id = c.user_type_id
   7: order by t.name, c.column_id, c.name

well, what can I say? beats my fat-finger-coded HTML pretty well. Go grab it!

 

Edit: I was having a couple of problems after all. Unfortunately not seen until published on the blog:

1) blogger seems to be adding extra line breaks inside the HTML as <br />. On the draft site, you can turn this off, but I am not sure how it's done when you publish using a third party tool. (Actually, it seems to work now, so maybe this is an account-specific setting. Others have also reported it to be working on and off)

2) I get line wrapping inside the code blocks. When using line numbers, this can be fixed by manually editing the inner div of the code block, setting the width to just large enough (I used 600 and 800px above). I don't think it's fair to blame this on the Code Snippet plugin. My blogger template might be more likely to causing this trouble. (I also saw problems like this with my hand-coded html)

Saturday, July 12, 2008

Part #1: Creating the Control Library

This is part 1 of a series of posts as described in the overview. In this post we will be creating a new Silverlight Control Library, to hold our first control. The control itself will only be a created as a stub here, but we will look closer into making something more useful in the next post. Please feel free to comment and ask question.

Think through it first!
In my opinion, it's important to stop up before doing almost anything. Think about what we are trying to accomplish and what the reasoning is which builds the foundation of our choises. For many of us, building a control library might be something you do on auto pilot. Then it just becomes even more important to also have the reasons clear in mind, if not only for oneself, so at least for anyone else seeing and trying to maintain your code.

So, what are we trying to accomplish?
  • We will build a Silverlight application to let users view and update some data.
  • The application will provide a user interface, where some portions of it has similar user interface and logic behind.
  • We are not only lazy, but also bored with repeating tasks, so we want to avoid repeating our code. (The real reasons are of course that we appreciate the aspects of reusability and scalability, and we want to allow for extensions and maintainess in one single place)
  • So, we like to make a control, encapsulating the user interface and the logic.
  • This can be done within our application, so why go the extra mile about a new control library?
  • The idea about this control is so great that we see it will also be used in future applications. Evenf we can copy the user control from one app to another, it's not real reuse, and it will have the same downside across applications as separate coding inside one application.

If having this discussion with yourself didn't convince you, you might think of another approach. If you are convinced, let's go ahead...

Doing it

  • Create new project in Visual Studio
  •  Drop the default class file
  •  Add a new class, with the name of your control
  •  Change the base class to ContentControl
  • Compile the project to ensure we have no errors yet.


Now, depending on a combination of confident, experience and self-esteam, we could go on and code for a week until everything is done. Or, we could test the code in small steps, getting a confirmation every time that what we have this far is working. My approach is to try and balance this, favouring frequent small tests over impressive show-offs.

This reminds me of my first programming lessons learned:

In the mid 80's, my parents was kind enough to by us kids a Commodore 64. It had a version of basic built in, and we learned to do small hello world apps. The code was limited to PRINT, INPUT and IF, with the exception of POKE 53280 and 1 which we considered to be the more avanced stuff.

In the owners manual, the last pages had some prints of program code. From what I remember, these were rather long, spanning several pages in a tiny font size. With what I see today as a greater confident than programming skills, I got on to the work of typing those cryptical lines into the computer. Several days later, I could finally type RUN...

I am not even sure if I knew what I expected, but certainly I wasn't too impressed with all this code to just write the line SYNTAX ERROR IN LINE 35 to the screen. After a lost battle of debugging I closed the book, turned off the C64 and forgot about programming for the next decade.

So, let's test that our one word of code actually works. To set this up, we will add a Silverlight Application test-project to the solution, and reference the control project.

1) Right Click on the Solution in Solution Explorer, choose Add New Project

2) In the dialog, chose a project type of Silverlight Application. Give the project a name, and click OK.

3) VS will prompt you to include a test-project or a test-page for the Silverlight app. Choose the second option, to have just a html page added to host your Silverlight Test Application.

4) Add a reference from the Test Project to the Control Library Project by right clicking the References node in the Solution Explorer. As we want the reference to be more dynamically, and for debugging to work across the two projects, we chose to add a reference to the project instead of just referencing the compiled assembly dll.

Your solution explorer should now look similar to this:

The next step is to add our control to the page:

1) Open Page.xaml by doubleclicking it from the Solution Explorer. You should see the file opens with a split screen, showing the layout (a plain white background on a gray surface) and some xaml-code.

2) In the xaml code, go to the root element and type in a new attribute, xmlns:qs. (qs is just my choise, and will be the xml namespace for controls in this page. Any name you prefer will do.) Intellisence should help you chose a value, and you should see your control library namespace and assembly in the list.

3) It might be worth noting that the item in the dropdown is using a more readable name than the actual value being inserted. The more logical value you see will look more like clr-namespace:QS.Silverlight.Controls;assembly=QS.Silverlight.Controls

4) Inside the Grid element of the same page, start entering a new tag name, starting with your prefix, like <qs: Intellisense will again kick in, and you should see your control in the dropdown.

5) Since we haven't done much to add any value to the base (ContentControl), we just use some of it's properties to see that it works. Try setting the Content proerty to a text and see if the preview makes you happy.

Now, everything should be ready for the final test. Right click the Silverlight Test Project (SLControlTest) and choose Set as StartUp project. Then hit F5!

Well, this means we are done for this time. You might say we haven't done much, and the control is adding much value over a ContentControl. Anyway, this was about creating the control library, so go ahead and give yourself a good pat on the back, have a coffee break and come back later for the next step: Create a Templated databound control.

Just one thing before you go... Note that I leave one section here for the things which are useful to add in at this stage, but not necessary for things to work. This can be good tips found on the net or valuable comments coming from my readers. Make sure you spend a minute here before going for that break.

Improvements:

- To be filled in when comments and new findings will add some value


Building a Silverlight Control - Overview

Travelling to distant places has been a part of my job the last few years. I find it inspiering to see and explore new places, and it often gives room for those extra moments of though. This time is a bit different, as I am back in Qatar for three months. Having been here in Doha a few times before, I feel I have seen the highlights already, and don't urge out every weekend to climb the sand dunes.

The good thing about this is that any time beside my real work can be spent in my apartment, hooked up to the internet, exploring new areas which are still new and unfamiliar. One place I have visited and spent most of my time in the last weeks are Silverlight 2 Beta 2. I find this new place very interesting. The weather is shining bright, but maybe a bit cloudy on some days. It takes a while to understand how to get around, but there are many helpful people doing their best to guide you around. The place is highly reccommended for a weekend, or better, a full week trip. The one downside might be that the area isn't too family friendly, so you might want to plan for some backup activities if you're not alone.

One thing I have been focusing on this time is to make a data bound, templated control. During my first little page, I found that I often need a list of records from the database and an edit form to update a single record. I have tried to encapsulate this functionality into a control while keeping it generic enough for broad use. Hopefully, my time and my small struggles can be useful to you readers, so I will publish a short series to cover the process I have been through:
  1. Creating a Silverlight Control Library
  2. Writing a Silverlight Control
  3. Using Linq2Sql to read and update data
  4. Customizing the control for use in an application
Once each part is ready, it will be a separate post. I will also update this post to link everything nicely together. I hope you enjoy and I will appreciate your feedback on suggestions or any bad coding you see.

Tuesday, July 8, 2008

Tips for making Templated controls work

As mentioned in yesterdays post, I was looking as Shawn Burke's ExpandoHeaderControl, as an example while trying to create my own. As I made a bit of progress I got more and more frustrated that his control wasn't really working in Beta2, so I spent some time debugging and trying to fix it. The process of getting it work is most useful if you'd like to download and play around with his project, but I realized it might also be of some use for general debugging. EDIT: Shawn Burke has posted an update on his blog, presenting a new version of his ExpandoHeaderControl. This new one is utilizing the Visual State Manager which not only simplifies the code, but also gives a better support for working the control state layout in Blend. The following steps contains direct references to mr. Burke's zip'ed control project (as I downloaded it today from his blog). He might already have had time to fix it, so please check his blog first for an update. Unless you have access to this project, or experience with problems in creating templatated controls, I bet you will find further reading rather meaningless. Consider yourself warned :)
  1. Open the project, answer oh yeah! to convert to Beta 2
  2. Try to recompile. Two errors will show and needs to be fixed: - On Line 95: ExpandoHeaderControl::OnApplyTemplate() should be public, not protected. (Change the word protected to public) - On Line 58: The last argument of DependencyProperty.Register() should be PropertyMetadata, not PropertyChangedCallback. (Change the word PropertyChangedCallback to PropertyMetadata) Unless you (or I) screwed up, the project now compiles.
  3. What seems (to me) to be a bug is that referencing a style for a custom control in App.xaml (<Style TargetType="c:myControl" ...) causes the loss of preview in our other xaml files, like Page.xaml. From my experience, this doesn't affect the runtime. Since preview is nice and the style set in App.xaml of this project is not really in use afterall, we just remove the whole <style> tag. At this point, you will see something in the xaml preview of Page.xaml, but it doesn't look so very cool yet.
  4. In the contructor of the control, add the following: this.DefaultStyleKey = typeof(ExpandoHeaderControl); Problem here is, without this, the default template will not be loaded from generic.xaml. Now, switch back to Page.xaml and enjoy the preview of Burke's cool work!
  5. We're almost there now... In Page.xaml, Change <ContentPresenter Content="{Binding}">to <ContentControl Content="{Binding}"> This should make the control work also in runtime :)

Good luck!

And again thanks to mr. Burke! I tried with no luck posting this as a comment on your blog last night. I will try again tonight...

Sunday, July 6, 2008

Templated Controls in Silverlight

Note: This post was just updated with regards to layout. I found some advantages in posting complete HTML code on draft.blogger.com, which I hope will help formatting future posts

Working on my small Silverlight test project, I found I was repeating myself quite a bit. Even if I always only write the best code, it's not worth being repeated :P So, I find I want to make a control to encapsulate my basic layout and functionality.

Before you read on, please bear in mind that I am a programmer with very little experience with Silverlight. I have seen strong indications that my quick decision on creating a control might not be the smartest choice afterall. The msdn have some good points that should be considered first. A good starting point for understanding templating and control inheritance at msdn is found here.

However, Silverlight do let us create such inherited controls. So, even if it not too wise in all scenarios, a little step-by-step walkthrough is still valid.

In my case, the application is data driven. I am using WCF and Linq-to-SQL to talk to my database, and Silverlight for my front end UI. (A valid question is really if Silverlight is right for this job, but that's not even important here :)

In many places I have a list of records in a ListBox. Each databound item has a few fields that I'd like to edit. Also, it should be possible to add new items, using the same edit form. Layoutwise, I let the edit form cover the ListBox while active, and hide it when not.

In this task of commonality, what I see varying are:

  • The caption over the ListBox
  • How each item in the ListBox is presented
  • The content of the edit form. (i.e. everything but the background and the cancel/save buttons)

Working a little with the built-in controls, I've noticed content can be set in XAML. And in this case, it's just about setting content a few places when using the control.

At this time, I am very optimistic and remembered from last night I read a post by Shawn Burke: Writing a Templated Silverlight 2 control. Conseptually, the ideas are similar, and mr. Burke's did a great job with this article. His ExpandoHeaderControl looks nice and overgoes the functionality I need in many areas. My starting point was to read his article and use the pieces I needed in my own project. I didn't download his code as this point, and I do as little copy/paste as I can, just as a personal preference while trying to understand what happens and get code working on my own.

The steps required to create such a control are:

  1. Create a class file containing the control code. (cs-file only, no xaml)
  2. Define which content can be set, and the corresponding properties
  3. Create a default template to provide the standard layout and functionality

From here, I had a few hours race downhill, sidetracing and backtracking so many times, I had problem remebering what I was trying to do. I will spare you the details and just give the very basic instructions for how to make this work here.

For this example, I will use the most simple and default setup of a Silverlight 2 Beta 2 project. I will push as few buttons and change as little as possible, hoping the understanding of the concept will be favored over a glossy look. (Besides, my artistic skills don't give me much of an option either :)

 

0. Create the VS project

After opening Visual Studio, create a new Silverlight Application Project and chose to add a web site to the solution when prompted. (The latter part won't make much of a difference, but it is what I did for my test)

With a little help from VS, I chose to call my application SilverlightApplication9 and the web site SilverlightApplication9Web.

 

1. Creating the class file

Adding the class file can be done in many ways, but right click the SilverlightApplication9 project node in Solution Explorer, choose Add Class and then just select Class from the dialog. (Description for this is An empty class definition)

EEven if I appreciate default naming and simplicity, I choose to name this file myControl.cs. This will make things less confusing later, and we should have a class of the same name.

Our control should inherit a ContentControl, so add the kolon and the name of the base class. Further, make a constructor and set the DefaultStyleKey property. This was the trick that ended my long lasting search this night, and was mentioned at the silverlight.net forums, by sladapter.

 

2. Defining which content can be set

The new control class needs to get a TemplatePart attribute for each templated content. In our little test, we will have two content properties, Content1 and Content2. Since Burke is right when stating it is a good idea to make string constants for the names, we do so as well.

For the actual content properties we also need a bit code. There is a code-snippet under the name propdp which is of grat help. (Type propdp - tab - tab)

Then, change

  • the type (int) to object
  • the name (MyProperty) to Content1
  • the owner type (ownerclass) to myControl
  • the last parameter (new UIPropertyMetadata(0)) to null

(This last one discloses the snippet was made for WPF, and would need a PropertyMetadata construct to wrap a callback handler for handling property change events. This will be necessary to use later, but not in this little test case)

Repeat the same steps for creating the property Content2.

This might be a good time to run a compile, just to see if everything works so far.

The final class definition might look similar to this:

[TemplatePart(Name = myControl.Content1Name, Type = typeof(ContentControl))]
[TemplatePart(Name = myControl.Content2Name, Type = typeof(ContentControl))]
public class myControl : ContentControl
{
 private const string Content1Name = "Content1";
 private const string Content2Name = "Content2";

 public myControl()
 {
 this.DefaultStyleKey = typeof(myControl);
 }

 public object Content1
 {
 get { return (object)GetValue(Content1Property); }
 set { SetValue(Content1Property, value); }
 }

 // Using a DependencyProperty as the backing store for Content1. This enables animation, styling, binding, etc...
 public static readonly DependencyProperty Content1Property =
 DependencyProperty.Register("Content1", typeof(object), typeof(myControl), null);

 public object Content2
 {
 get { return (object)GetValue(Content2Property); }
 set { SetValue(Content2Property, value); }
 }

 // Using a DependencyProperty as the backing store for Content2. This enables animation, styling, binding, etc...
 public static readonly DependencyProperty Content2Property =
 DependencyProperty.Register("Content2", typeof(object), typeof(myControl), null);
}

 

3. Create a default template for the control

To the same project (SilverlightApplication9), add a new xml file and name it generic.xaml. In this file, remove any existing content and replace with this:

<ResourceDictionary  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:c="clr-namespace:SilverlightApplication9"
  >
</ResourceDictionary>

 Note the clr-namespace is referencing the project name, so change this if you chose a better name for your project.

Inside the ResourceDisctionary, build the layout template for your control. A simple example might look like this:

<Style TargetType="c:myControl">
 <Setter Property="Template">
 <Setter.Value>
 <ControlTemplate TargetType="c:myControl">
 <Grid>
 <Grid.RowDefinitions>
 <RowDefinition></RowDefinition>
 <RowDefinition></RowDefinition>
 </Grid.RowDefinitions>
 <Canvas Background="Green" Grid.Row="0">
 <ContentControl Background="Blue" Content="{TemplateBinding Content1}"></ContentControl>
 </Canvas>
 <Canvas Background="Yellow" Grid.Row="1">
 <ContentControl Background="Blue" Content="{TemplateBinding Content2}"></ContentControl>
 </Canvas>
 </Grid>
 </ControlTemplate>
 </Setter.Value>
 </Setter>
</Style>

 Notice how c:myControl is used. (c is from the xmlns:c attribute we just set in the ResourceDictionary element)

The placeholders for our content are the two ContentControl elements. From the TemplateBinding syntax, you see one refer to Content1 and the other to Content2.

Just a few more steps before we leave this file:

  • Select the file (generic.xaml) in the Solution Explorer and choose Build Action to be Resource in the properties list.
  • Open your AssemblyInfo.cs and add the following line:
    [assembly: XmlnsDefinition("http://schemas.microsoft.com/client/2007", "SilverlightApplication9")]

I am not sure about this last one, as everything seems to work fine without it. I did however find it mentioned from so many posts in the silverlight.net forums, some even saying it was a bug that caused our applications to work without it. Well, better leave it in there then :) Really, I think this is how controls in an assembly can be referenced without using a namespace prefix, like the built-in controls. We are however using a prefix, so I can't quite understand why this must be needed. Any comments on this are most welcome.

 

4. Done!

Now, that's all :D

Just to prove it works, edit your Page.xaml:

Add attribute to root UserControl element: xmlns:c="clr-namespace:SilverlightApplication9"

Add your control to the page, defining the two templates:

<c:myControl>
 <c:myControl.Content1>
 <TextBlock>This is Content1</TextBlock>
 </c:myControl.Content1>
 <c:myControl.Content2>
 <TextBlock>This is Content2</TextBlock>
 </c:myControl.Content2>
</c:myControl>

Hopefully, you see a rather ugly, but boring layout with two rectangular areas in green and yellow stacked on top of each other. Each one should have it's text, as given in the control content elements above. As anywhere else, from here only you imagination will be your limit to what you can accomplich... enjoy!