After having written mhy previous entry about how to style and template controls, I guess it is a good time to have a look at how to create controls that are template- and styleable. (Can you write "template- and styleable"? Looks weird...well...I am swedish so I'm allowed to write less than perfect english) Unfortunately, due to my lack of imagination, I don't have a really cool control to build and show you. So instead I'm going to create a very limited control that will work more or less as a simple button.
To start off, create a Silverlight Application project in Visual Studio 2008. When asked if a separate webproject is needed for debugging, just go with creating one on the fly. For this exercise there isn't really a need to make changes to the hosting page, whichthat means that VS can just as well create the page on the fly. After VS has created the Silverlight Application project, add another project to the solution. But this time. add a Silverlight Class Library. This isn't necessary, but having the control in a separate assembly makes re-useable. (Probably won't re-use it considering what the control does, but it keeps it real...)
For now, just ignore the application project and focus on the library. Start by removing the Class1.cs file and add a new class called MyButton. Yes...MyButton...it sucks I know, but it works... Start off by modifying the inheritance of the class. Change the class so it inherits from Control to start with. This will be changed later, but start like this.
Before moving further, there has to be a layout for the control. This should of course be Xaml. It is possible to create the layour from code, but that removes the possibility of templating the control and because of this the Xaml way is the correct way. Adding a default template to the control is not that hard, but not really obvious. Start by adding a folder called "themes" in the root of the application. Inside the themes folder, add a new Xaml file. This is easiest to do by adding an Xml file and just renaming it. It has to be named "generic.xaml". After the file has been added, some changes to it's build action has to be made. Select the file in the Solution Explorer and take a look at the Properties pane. Now there are aparently at least two settings that work here, but the oficial one is the one I will use. Set the "Build Action" to "Resource" and make sure that the "Custom Tool" part is empty. (The other way is to set the "Build Action" to "Page" and then leave/set the "Custom Tool" to "MSBuild:MarkupCompilePass1". This worked for me, but I wouldn't recommend it)
The content of the "generic.xaml" file has to start off by looking like the following:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
</ResourceDictionary>
So if the "generic.xaml" file was started by adding an Xml file, the Xml declaration at the top has to be removed first. Now that the "generic.xaml" has been turned into a resource dictionary, it is possible to add a default template to the MyButton control. As said in the previous post, a template is actually a specific setter inside a style, so the resource dictionary needs a style. But since the style needs to have its TargetType to point at the MyControl control we have to make a small modification to our Xaml. To be able to reference the MyButton control, an xml-namespace pointing towards the correct namespace has to be added. An xml-namespace with a clr-namespace reference is sort of like adding a "using" statemen. After the xml-namespace has been added, it is possible to add the style and point it to the right target type.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyButton"
>
<Style TargetType="local:MyButton">
</Style>
</ResourceDictionary>
Next it is time to set the template of the control by adding a setter that sets the "Template" property of the control. It is going to be a very simple template, but it will show the idea.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyButton"
xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"
>
<Style TargetType="local:MyButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MyButton">
<Grid Background="{TemplateBinding Background}">
<Rectangle>
<Rectangle.Fill>
<SolidColorBrush x:Name="BackgroundBrush" Opacity="0" />
</Rectangle.Fill>
</Rectangle>
<TextBlock Text="{TemplateBinding Text}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{TemplateBinding Foreground}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
The template contains 3 template bindings. A template binding will casue the value of the property with the binding to have it's value set by a property in the control. So the "{TemplateBinding Foreground}" will set the TextBlock's foreground to the foreground set in the control. A whole lot of properties are already available on the control due to its inheritance. The most interesting template binding is the one that binds the TextBlock's Text property. THis is interesting since there isn't a property called "Text" on the MyButton control. This has to be created, but first the control has to tell the system that it should use the new style/template.
In the constructor of the MyButton class, add a line of code that sets the "DefaultStyleKey" to "typeof(MyButton)". This tells the system that this control wants to use the style in the "generic.xaml" that has a TargetType that corresponds to this type. That's all that is needed.
public MyButton()
{
DefaultStyleKey = typeof(MyButton);
}
If there is any interest in fetching parts of the template and handle those, this can be done by overriding the OnApplyTemplate() method and then getting hold of the parts by calling GetTemplateChild() passing in the name of the part. There will be more about parts and states in the next part of this tutorial...
Now, to the Text property. To be able to use a template binding, the property must be implemented as a DependencyProperty. This isn't very complicated, but necessary to make bindings work. Start off by adding a public and static DependencyProperty to the MyButton class. The naming standard says that it should be named the same thing as the property itself, but with a "Property" at the end. So in this case it should look like this:
public static DependencyProperty TextProperty;
A DependencyProperty isn't created, it is registered. In the registration the name of the property, the type of the property and the "owning" type must be supplied. The "owning" type is the type that declares the property. It is also possibe to give the property a default value as well as register a callback method that is called when the property is changed. The complete DependencyProperty declaration looks like this:
public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(MyButton), new PropertyMetadata(""));
Next the actual property of the MyButton type needs to be implemented. Since MyButton inherits from DependencyObject, it has one method for getting the value of a DependencyProperty as well as one to set one. They are, not surprisingly, called GetValue() and SetValue() and works with object. So the property should look something like this
public string Text
{
get { return (string)GetValue(MyButton.TextProperty); }
set { SetValue(MyButton.TextProperty, value); }
}
Now the layout of the button is actually done and it can be tested. There still has to be some functionality added to the control, but it will now show up if added to a page. So add a reference from the Silverlight Application project to the MyButton project. Then open up the Page.xaml file and add an xml-namespace pointing to the new assembly and also add an instance of the button. It looks like this:
<UserControl x:Class="MyButtonTest.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:btn="clr-namespace:MyButton;assembly=MyButton"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<btn:MyButton Width="100" Height="50" Text="Hello World" Background="Yellow" Foreground="Black" />
</Grid>
</UserControl>
This should give you a view of how the control looks. The final thing to do is add some functionality. There is no use creating a control, if it doesn't have any functionality. The functionality added in this part of the 2-part tutorial, is a click event. Not very exciting, but it is some functionality. The Click event will be based on MouseLeftButonUp combined with a bool keeping track if the mousebutton was pressed down on the control too. Just handling MouseLeftButtonUp means handling a situation where the mousebutton was pressed outside of the control and then released on top of this control. So start off by adding a bool to keep track if the button has been pressed. Then add eventhandlers for MouseLeftButtonDown and Up as well as for the MouseLeave event. In the "down" handler, set the bool to true. In the "leave" set it to false. And in the "up", check to see if it is true. If it is true in the "up" event it is time to raise the Click event. Before it can be raised, it of course has to be added. Then of course set it to false before exiting the handler The code looks like this:
bool _mousePressed = false;
public MyButton()
{
DefaultStyleKey = typeof(MyButton);
this.MouseLeave += new MouseEventHandler(MyButton_MouseLeave);
this.MouseLeftButtonDown += new MouseButtonEventHandler(MyButton_MouseLeftButtonDown);
this.MouseLeftButtonUp += new MouseButtonEventHandler(MyButton_MouseLeftButtonUp);
}
void MyButton_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_mousePressed = true;
}
void MyButton_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (_mousePressed)
{
OnClick();
}
_mousePressed = false;
}
void MyButton_MouseLeave(object sender, MouseEventArgs e)
{
_mousePressed = false;
}
protected void OnClick(MouseEventArgs e)
{
if (Click != null)
Click(this, e);
}
public event EventHandler<MouseEventArgs> Click;
This code is the final code for the control for this part of the tutorial. I know that the idea of setting the _mousePressed to false on MouseLeave stinks, but it's the simple way of handling it. It should probablybe handled a bit more logical, but that isn't really the goal of this blog post. The post is about the mechanics of creating a control, not about how to build a good button. There is already a button in the framework, so the control is not really useful anyway...
To see that it works, compile the project and add a click handler to the MyButton control in the Silverlight Applications Page.xaml. Something like this:
Page.xaml
<btn:MyButton Width="100" Height="50" Text="Hello World" Background="Yellow" Foreground="White" Click="MyButton_Click" />
Page.xaml.cs
private void MyButton_Click(object sender, MouseEventArgs e)
{
MyButton.MyButton btn = (MyButton.MyButton)sender;
btn.Text = "Hello again";
}
The next part will be about the parts and states model. Mostly about states, but parts will be mentioned. This control doesn't have any parts which makes it a bit hard to talk about, but it will have some states added to it so that the look can be changed when the mouse is over, the mousebutton is pressed and so on. Stay tuned for that...