Welcome to WindowsClient.net | My Blog | Sign in | Join

Nidonocu - typeof(Tea)

Random discoveries as I play with WPF

Sponsors





  • advertise here
Amuse Alpha now on CodePlex

I've been working on and off for the last few years since I started programming on an MU* client which I call Amuse.

Main Screen

It took creating a ticking clock in the form of a CodePlex project creation (you get 30 days to set up your project before it is either deleted or you publish it) for me to really dive on in and clean out the code, but I did it and I'm pleased to make it public today.

The current version as it looks right now isn't that impressive to the eyes but I've spent a lot of time making sure the underlying code for it is sound and should help give me less headaches as I now focus on moving it towards a 1.0 state, complete with a decoupled user interface so that when the time is right, a far nicer one can replace the one you can see here.

More posts as I develop this application should follow in the future, and I may well link this blog to the the project via an RSS link if I start using it a lot to discuss the development of Amuse.

For now anyway, check out the Project Homepage and if by chance you enjoy some classic text based RPG's, try it out and see how it handles.

Property beats style, style beats theme, theme beats scissors?

No posts in.. quite a while. Oh dear. I really should stop trying to do complex things like trying to do a series and just do what I set out to do, post little discoveries of 'oh, so that's how it works' while I code.

So here is one:

A MultiDataTrigger trigger lets you define a set of rules that when all are true, will make updates to a user interface.

In my case, I created one that would enable the commit button of my dialog box if all the required fields had been successfully filled in and validated:

<MultiDataTrigger>
 <MultiDataTrigger.Conditions>
  <Condition Binding="{Binding ElementName=textBoxWorldName,
                      Path=Text,
                      Converter={StaticResource StringLengthToBooleanConverter}}"
                      Value="true" />
  <Condition Binding="{Binding ElementName=textBoxWorldAddress,
                      Path=Text,
                      Converter={StaticResource StringLengthToBooleanConverter}}"
                      Value="true" />
  <Condition Binding="{Binding ElementName=textBoxWorldPort,
                      Path=Text,
                      Converter={StaticResource StringLengthToBooleanConverter}}"
                      Value="true" />
  <Condition Binding="{Binding ElementName=textBoxWorldPort,
                      Path=(Validation.HasError)}"
                      Value="false"/>
 </MultiDataTrigger.Conditions>
 <MultiDataTrigger.Setters>
 <Setter Property="IsEnabled"
         Value="True" />
 </MultiDataTrigger.Setters>
</MultiDataTrigger>

This code is correct. However, for some reason, it still wasn't working.

I puzzled over why, did some digging on the MSDN forums and discovered why.

A MultiDataTrigger can only be used inside a Style for an object. (It can't be set in Button.Triggers for example).To set the default state for the button (false until otherwise set true), I did the following:

...
<Button Content="_Start Connection"
        Width="Auto"
        MinHeight="24"
        Margin="10,10,10,10"
        MinWidth="92"
        x:Name="buttonCommit"
        IsEnabled="False"
        IsDefault="True">
 <Button.Style>
  <Style TargetType="{x:Type Button}">
   <Style.Triggers>
    <MultiDataTrigger>
    ....

The IsEnabled property that I set within the Button element, overrode any change I wanted to make using the MultiDataTrigger which was located inside the style. As local set properties override Style, even if the Style is locally hosted within the element's Style property, it is an extra layer on top of the Style Override tree I mentioned in a previous post.

The fix of course, is move the setting of that property in to the Style its self, thus any Triggers have the full right to override it. As so:

...
<Button Content="_Start Connection"
        Width="Auto"
        MinHeight="24"
        Margin="10,10,10,10"
        MinWidth="92"
        x:Name="buttonCommit"
        IsDefault="True">
 <Button.Style>
  <Style TargetType="{x:Type Button}">
   <Setter Property="IsEnabled"
           Value="False" />
    <Style.Triggers>
     <MultiDataTrigger>
     ...

PS: If you came here and wanted information about the Converter I'm using there, let me know in a comment and I'll whip up a quick post.

SizeToContent border bug

This isn't part three of my theme series as I promised, but instead a random discovery which is after all what I subtitled this blog with. :) I'll get back to that in due course!

Instead, while building a dialog box today, I came across a very weird bug which seemed to have apparent cause. Can you see it?

Border Bug

Those with keen eyes will spot a distracting single pixel width black border along the inside edge of the window on the right and bottom sides (click the image to see it a bit clearer at the original size). This only seemed to kick in once I enabled the SizeToContent property and set it to WidthAndHeight.

SizeToContent is something very useful and not something I wanted to disable, it would let my window resize according to the content inside it which means if the user opens that expander, the window would grow to account for the extra space needed. All very WPF.

None of the child controls were causing it either, as you can see, I had not yet started to style the dialog yet.

After some poking around, I pinned down an option that seemed to be a workaround solution, enabling the SnapsToDevicePixels property on the Window:

In Expression Blend

Or in XAML:

<Window x:Class="Nox.Amuse.Dialogs.QuickConnectSetupDialog"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Quick Connection" ResizeMode="NoResize"
    WindowStartupLocation="CenterOwner" x:Name="DialogWindow"
    SizeToContent="WidthAndHeight"
    ShowInTaskbar="False" SnapsToDevicePixels="True">

As far as I can work out, the bug was caused by my window sizing down to my content, but not quite enough (half a pixel or so, which renders as the greyish single pixel-width lines). SnapToDevicePixels does what it says on the tin, it forces the window to clip down to whole pixels. Feel free to comment if you I've not got this explanation quite right!

After applying the property and running again, I just have the regular border again:

All Gone!

Hope this helps others who get caught by this one!

WPF - Themes and Control Libraries - Part 2

Oops.. how did that happen? I said one post a week, so its about time I posted again so that I don't create another abandoned blog! And 5am on a snowy March morning seems about as strange a time as any to try writing some code, so here we go!

Click here for Part 1

How themes work

In using WPF, you will know if you apply a style at the point at where you use the control, it overrides the built in one. This continues when you use themes.

Style Overrides 

When you create a custom control, a style is automatically created in the Generic.xaml resource dictionary. If you don't want a control to be themeable, you can edit this to make it as pretty as you like without it being affected by the user changing their system theme.

Now, if we want a theme aware control, we will need to make a file for each style we are going to support. These are the files that in the above diagram I've simplified to Named Theme Style. No named theme style will ever override another. For example, if you don't include an Luna.Metallic (the Windows XP silver theme), it won't fall back to Luna.NormalColor (the blue and green theme).

There is one exception to this as you can see, the Classic style. If no named theme file is found but a Classic file is, that will be used, not the Generic theme.

I point this out because if your application is loaded on to a system where the user is using an unsupported theme then they will end up with a classic look instead of a more modern look that you have likely provided in your named theme files.

Examples of themes where you might forget to include a style dictionary for them include official visual styles like the Media Center Royale theme or the Zune theme, and unofficial visual styles where the user has patched their uxtheme.dll file to enable the system to run non-Microsoft visual styles. In the latter case you have no idea what the name of theme could be so there is nothing you can do unless you override the entire theme system which is something I will cover how to do in the final part of this tutorial.

Finally of course, Instance styles still remain the most important, by using code such as:

<custom:CustomButton Content="Hello" SecondaryText="WindowsClient.net">
 <custom:CustomButton.Style>
  <Style>
   <!-- Style Definition -->
  </Style>
 </custom:CustomButton.Style>
</custom:CustomButton>

You can override your own styles just the same as if you were overriding styles for the built in controls.

Now that you understand how themes 'fall back' in to one another, we can go ahead and make one!

Creating a named theme

For each named theme, you will need a resource dictionary with a specific name so that the theme system can find it.

To create your resource dictionary, right-click the Themes folder in your Control Library Project. Point to Add and then click Resource Dictionary.

Create Resource Dictionary

What you do next exactly will depend upon if you are running Windows XP or Windows Vista.

If you are running Windows Vista:

Name the file Aero.NormalColor.xaml

If you are running Windows XP:

Name the file Luna.NormalColor.xaml

Then click Add to create the file. I'll refer to from now on as the Named Style file.

Visual Studio will provide you with a template file, but to give us a better starting point, delete the contents of the new file and copy the entire contents of Generic.xaml in to your new Named Style. This will make sure you include the local namespace definition in Generic's ResourceDictionary element.

Now, lets change the look of this style so we can see a difference. In the Named Style file, replace the ControlTemplate element with the following to give it a unique look. You can of course, edit this if you want or come up with something of your own design, just make sure the change is obvious.

<ControlTemplate TargetType="{x:Type local:CustomButton}">
 <Border BorderBrush="White"
         BorderThickness="1">
  <Border BorderBrush="Black"
          BorderThickness="1">
   <Border.Background>
    <LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
     <GradientStop Color="DeepSkyBlue" Offset="0"/>
     <GradientStop Color="LightBlue" Offset="1"/>
    </LinearGradientBrush>
   </Border.Background>
   <Border BorderBrush="White"
           BorderThickness="1">
    <Grid>
     <Grid.RowDefinitions>
      <RowDefinition Height="0.5*"/>
      <RowDefinition Height="0.5*"/>
     </Grid.RowDefinitions>
     <ContentPresenter HorizontalAlignment="Center" Grid.Row="0"/>
     <TextBlock HorizontalAlignment="Center" Grid.Row="1"
                Text="{TemplateBinding SecondaryText}"/>
    </Grid>
   </Border>
  </Border>
 </Border>
</ControlTemplate>

Now, you might think that's it, but if you build and run your application right now, you will see that nothing has changed. There is one small piece of code you have to chance to kick in the theme system.

Enabling the theme system

This step is quite important and easy to get confused over when you introduce the complexity of a control library. Setting this wrong can even cause your application to crash if it fails to find a theme, as I discovered when working on the application that prompted me to create this tutorial.

To enable the theme, open your AssemblyInfo.cs file. This is normally hidden under the Properties item and can found by clicking the expander + by the Properties item for your Control Library in the Solution Explorer and then double clicking AssemblyInfo.cs to open the file.

Finding AssemblyInfo

Once inside that file, locate the following section of code, it is normally around line 34:

[assembly: ThemeInfo(
    ResourceDictionaryLocation.None, //where theme specific ...
    //(used if a resource is not found in the page, 
    // or application resource dictionaries)
    ResourceDictionaryLocation.SourceAssembly //where the generic ...
    //(used if a resource is not found in the page, 
    // app, or any theme specific resource dictionaries)
)]

You need to change one value here, the first ResourceDictionaryLocation parameter, to SourceAssembly. If you like, you can strip out the comments too and leave the line now reading as:

[assembly: ThemeInfo(
    ResourceDictionaryLocation.SourceAssembly,
    ResourceDictionaryLocation.SourceAssembly
)]

I should note that making these changes to your .exe's AssemblyInfo.cs file instead of your .dll's, is the thing that in my case caused the crash. So I recommend keeping either all your custom controls in the .exe or all in external .dll assemblies.

Now before you build or run your application, make sure your system theme is set to the one you coded for (on Windows Vista, Aero.NormalColor is used regardless of if you running with or without glass). If it is set correctly, then running the application should produce this:

The themed control

Now, just to give it a test, close the application and repeat the steps above but this time, create a new resource dictionary called Classic.xaml in the Themes folder.

Change the appearance of the control to give it a suitable look (the source for my Classic.xaml can be found in the zip at the end of this post). Then change your system theme to Windows Classic or Windows Standard.

To change your theme under Windows XP:

  1. Right-click the desktop.
  2. Click Display Properties.
  3. Click the appearance tab.
  4. Select Windows Classic style from the Windows and Buttons combo box.
  5. Select a colour scheme if you wish.
  6. Click OK.

To change your theme under Windows Vista:

  1. Right-click the desktop.
  2. Click Personalize.
  3. Click Window Color and Appearance.
  4. If you are currently running Aero glass, click the Open classic apperance and properties link.
  5. Select Windows Standard or Windows Classic from the Color schemes list.
  6. Click OK.

If all goes well, when you run your application, you'll see something like this:

Classic

Now, to see the theme change occur, leave the application running and reset your visual style back to either Aero or Luna Blue. A few moments after your system theme changes, your application should automatically update its own appearance.

In the final step, I'll look in to how to force the theme system to select a different theme to deal with the edge cases of unsupported themes. I'll also look in to accessing the current Visual Style information through code so you can make manual adjustments to your application when using the theme system alone isn't convenient.

Themed Application Solution (Part Two)

Zip File - 26KB

WPF - Themes and Control Libraries - Part 1

Anyone familiar with the Windows Presentation Foundation will know you can make a control look like anything you want. This feature can be used to great effect to give your application a unique look and feel. It can also be abused however and make an application stick out like a sore thumb.

To help combat this, you might want to consider using Themes in your application. Themes are a collection of WPF styles that are automatically selected when the user is using a specific Windows theme. You can see themes in action by dropping a Button control on to a Window, running the application and then, for example, changing your system theme from Luna or Aero, to Classic. The button will gain that boxy look of the Windows 2000 era.

 Automatic Themes in action

There are videos and guides out there on how to use themes (this one on WindowsClient.net is a good example and what I used as a starting point) but they all assume that your controls are in the same assembly as your main executable and often in 'real world' applications, this is not the case.

What follows is a step by step guide in creating an application with a custom control in a library, applying different themes and using the control in an application.

Getting Started

New Project WindowCreate your solution and choose WPF Application from the list of templates. I am using Visual C# 2008 Express Edition so don't be surprised if your new project window is quite different from the one pictured here.

Next you will want to add the Control Library project to your solution, if like me you are using the 2008 Express Edition, you will notice someone at Microsoft forgot to include this template. Choosing the basic Class Library template instead though can be a pain later when you are trying to add WPF related files and their templates are not in the listed in the Add New Item window.

Instead, add another WPF Application Project to the solution by right-clicking the solution name at the top of the Solution Explorer, pointing to add and then clicking New Project. You may be prompted to save the solution. When you have done that, select WPF Application and give this new 'application' project the name of ControlLibrary or similar.

You must then change the project type to a Class Library. To do this, double click the Properties item in the library project and on the Application tab of the Properties screen, find Output type and select Class Library from the list of options. You should then also delete the App.xaml and Window1.xaml from the library project.

Changing the Output Type

Finally, add a reference to the library in the application project by right-clicking the References item in the application project and clicking 'Add Reference'. Then from the Add Reference window, click the Projects tab and select the Control Library project. Then click OK.

Creating the Control

Add New Item Window To create the custom control, right-click the Control Library Project in the Solution Explorer, point to Add and then click Add New Item. From the list of templates, choose Custom Control (WPF) and type a name for the control. In my case, I'm going to make a special button so I've called it CustomButton. Make sure not to chose a User Control or User Control (WPF), these are quite different things all together.

When you click Add, Visual Studio will suddenly do a number of things which you might not be familiar with if this is your first custom control.

  • Firstly it will create a CustomButton.cs file. This is expected, but note it is just a code file, not a XAML file with a code-behind file like the Window1 or App files.
  • It will also create a folder in your project called Themes and in to that place a Resource Dictionary called Generic.xaml. This is where the look and feel of all the custom controls you make is stored. If you haven't used a Resource Dictionary before, think of it as a CSS file for your control. It only stores its appearance, not how it works.
  • Finally, a reference is added to your library project to the .net library UIAutomationProvider. This library lets you add accessibility features to the control and also test the control later if you so wish.

The next step is to think about the custom function we want the control to do. In my case, I want to add a specific text field to my button so it will have a secondary label to what ever we decide to set as the main content for the button. This is a bit of a silly example, but it is simple to make so lets go with it.

Open the CustomButton.cs file if it is not already open and delete the giant helpful XML comment that's been added by the template. Replace it something more concise such as:

/// <summary>
/// Defines a button with a secondary text field
/// </summary>

Next change the type that the CustomButton class inherits from. Change it from Control to Button. This will give us all the basic functions of a standard Button control (like Click events and so on) for free.

public class CustomButton : Button

Finally, we want to provide a place for the button to store the extra text for the secondary label. For that we will use a string stored in a Dependency property so that all changes to the text cause the user interface to update automatically.

///<summary>
/// Gets or sets the secondary text to display.
/// This is backed by a Dependency Property.
///</summary>
public string SecondaryText
{
  get { return (string)GetValue(SecondaryTextProperty); }
  set { SetValue(SecondaryTextProperty, value); }
}
///<summary>
/// This is the backing Dependency Property for the SecondaryText property.
///</summary>
public static readonly DependencyProperty SecondaryTextProperty =
  DependencyProperty.Register("SecondaryText", typeof(string),
  typeof(CustomButton), new UIPropertyMetadata(default(string)));

I won't go in to detail about Dependency Properties since there are plenty of things out there that can explain them better. However, I have also included a Code Snippet file at the end of this entry that you can use to quickly create these from a code template.

There! We've made the control! That was easy.

Of course right now if we tried to use this control, we wouldn't be able to see anything so let's create a basic look for our control.

Create a Generic Style

To start working on a basic style for the control, open the Generic.xaml file located in the Themes folder of the ControlLibrary project.

You can see that a basic style has been created for you already:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Nox.Blog.ControlLibrary">
 
 
  <Style TargetType="{x:Type local:CustomButton}">
   <Setter Property="Template">
    <Setter.Value>
     <ControlTemplate TargetType="{x:Type local:CustomButton}">
      <Border Background="{TemplateBinding Background}"
              BorderBrush="{TemplateBinding BorderBrush}"
              BorderThickness="{TemplateBinding BorderThickness}">
      </Border>
     </ControlTemplate>
    </Setter.Value>
   </Setter>
  </Style>
</ResourceDictionary>

Again, its important to remember that this file is shared for all the custom controls that you make in this library so as you add more custom controls, this file will grow.

Lets make some changes to the template this style uses so that the control displays the content and secondary text values for the control in a simple box.

Change the Border element inside the ControlTemplate so that it reads as follows to add a visible box.

<Border BorderBrush="Black"
        BorderThickness="1">

Next, we want to add a Grid to layout the contents of our control, a ContentPresenter to show the Content Property and a TextBlock for the SecondaryText Property. Start by adding the following inside the Border element:

<Grid>
 <Grid.RowDefinitions>
  <RowDefinition Height="0.5*"/>
  <RowDefinition Height="0.5*"/>
 </Grid.RowDefinitions>
 
</Grid>

This defines a Grid with two rows which have half the available space each. Following on from that, add a ContentPresenter and TextBlock inside the Grid with the following attributes:

<ContentPresenter HorizontalAlignment="Center" Grid.Row="0"/>
<TextBlock HorizontalAlignment="Center" Grid.Row="1"
           Text="{TemplateBinding SecondaryText}"/>

The first two attributes of each element are fairly clear. ContentPresenter automatically gets its content from the Content property of the control so requires no further attributes to work correctly. TextBlock though needs a template binding to the SecondaryText property we created in order to know what to display. Template binding to a Property you create is no different from binding to a built in one.

Not very exciting but it will do for now, lets actually see what that looks like.

Using the control

If you read the 'giant helpful XML comment' before you deleted it, you'll already know this bit, but if you didn't, here's how to use the control in the application half of your solution.

First, open the Window1.xaml file in the application project. I myself renamed this to MainWindow.xaml so watch out for that in the rest of the code I post.

If you also rename the Window, remember to update the references to it in the x:Class attribute, the names in the code-behind file and the reference to it in the App.xaml file.

MainWindow by default looks a little something like this:

<Window x:Class="Nox.Blog.ThemedApplication.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
</Window>

If you look at the top, you can see that the file has already defined two namespaces (using the xmlns attribute). The default one for all the built in WPF elements and one named x for other important framework items. We must add our own namespace for the custom control library so that we can reference the items in that. To do that, change the Window element to the following while taking in to account the names you gave the ControlLibrary in your solution.

<Window x:Class="Nox.Blog.ThemedApplication.MainWindow"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:custom="clr-namespace:Nox.Blog.ControlLibrary;assembly=Nox.Blog.ControlLibrary"
 Title="Themed Application" Height="300" Width="300">

If we break down this line, we can see the following:

  • xmlns:custom - This says we are creating a new namespace and that it will be using the name 'custom'.
  • clr-namespace:Nox.Blog.ControlLibrary - This indicates the contents of the namespace come from a code namespace in our application, this is similar to a 'using' statement at the top of a C# code file. If we were accessing a code namespace in the application assembly, we could stop here.
  • ;assembly=Nox.Blog.ControlLibrary - Since we are not accessing a code namespace inside the application assembly (the EXE), we need to give the name of the DLL file that the control resides in. In this case, its the same as the code namespace, however it could be different. For example, you might compile the library to just CustCtrls.dll in which case, this section would read ;assembly=CustCtrls instead.

I also took a moment to change the Windows's Title attribute to Themed Application for presentation. :)

Finally, the last line. Place this directly inside the Window element:

<custom:CustomButton HorizontalAlignment="Center"
                     VerticalAlignment="Center"
                     Content="Hello"
                     SecondaryText="WindowsClient.net"/>

Let's break this down. First it starts with the custom: namespace identifier that we set in the last step. Then the name of the control, CustomButton. Then we set the Horizontal and Vertical Alignment. These along with the Content attribute are defined by Button and its parent types which we inherited from. Finally we set the SecondaryText attribute which again, doesn't need any special treatment and can be used as easily as the built in properties.

If you want, you can also set the Content by doing something silly like this:

<custom:CustomButton HorizontalAlignment="Center"
                         VerticalAlignment="Center"
                         SecondaryText="WindowsClient.net">
      <CheckBox/>
</custom:CustomButton>

SecondaryText however must be a string since that's what we defined it as in the code file.

If all goes well, pressing F5 in Visual Studio to Build and Run should give you something like this:

 The Generic style displayed

Now at this point, if you had no intension of making the control themeable, we could stop here. Just continue to edit the Generic.xaml until we have a really nice looking control. Instead, in the next part, we'll have a quick look at how the theme system works and then dive in to it.

Notes: I originally was going to make this first post contain both the custom control creation and theme parts, however, this has turned already in to quite a monster so breaking it up seemed to make sense. Please leave a comment if you found this useful or if you think I could improve on any parts of it!

Themed Application Solution and Code Snippet (Part One)

Zip File - 25KB
One last try

Okay, so I think I've tried to start a coding and programming blog at least twice already now and I realised that just talking about what I'm working on wasn't interesting. The thing that made other blogs interesting was the useful content they provided on how their authors solved problems in what ever it was they were working on.

So with that in mind, let me make this one of few non-educational posts and just quickly give a quick set of blog meta data for future reference by those curious.

Who?

I'm Nidonocu, or James to less interesting people. I was born and still exist in the UK. Now living specifically in Sheffield where I moved to attend Sheffield Hallam University for a networking foundation degree. While working on that, Microsoft released the Express editions of Visual Studio and from there I quickly found learning to write software in that to be a nice distraction from an informative but sometimes dull university course. ;)

Outside of coding I also enjoy playing Halo 3 and other titles on my Xbox 360 and Wii, writing the (or should that be an?) odd novel and drawing things.

What?

I started with Visual Basic.net, then moved on quickly to C# and Windows Forms. I'm now working with C# 3.0, .net 3.5 and of course the Windows Presentation Foundation. I've been working on a long term project of a MU* client which through the process of making it I've learnt a lot about the platform and software development and design in general. I'm also now building a kiosk style application for a convention with some interesting attributes and a deadline so that will likely grab most of my focus for the next couple of months.

When?

I hope I can post something interesting at least once a week. Far better to have something interesting and regular rather than lots of boring useless posts.

Over the next few days I'll look in to styling this blog and adding other things like a blogroll and that. For now I'll just say thanks to the WindowsClient.net team for the blog space and to Rob Relyea for mentioning this new space to write in.

Posted: Feb 12 2008, 01:27 PM by Nidonocu
Filed under: ,