.Net WPF – Dynamically Set DataTemplate

0 Comments

After a member posted a topic on Dream.In.Code on how to make the last item in a ListBox have a different “look” than the other items, I thought this would be a great time to do a tutorial on how to dynamically set the DataTemplate of an item. There will be very little code behind(just to populate a list for the ItemsSource of the ListBox). We will be using the DataTemplateSelector property of the ListBox.

So first, in my example, I am going to create a very, very simple class called Person, and it will have one property called FullName. This class will implement the IEquatable interface to allow comparison between two Person objects.

Person.cs

class Person : IEquatable<Person>
{
	public string FullName { get; set; }

	public bool Equals(Person other)
    {
        if (other == null)
            return false;

        return (this.FullName == other.FullName);
    }
}

Next, we need to create a class the inherits from DataTemplateSelector so we can override the SelectTemplate method to use our own logic to determine the DataTemplate for the item.

public class ListBoxDataTemplateSelector : DataTemplateSelector
{

}

So now, we will override the SelectTemplate method.

public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
    FrameworkElement element = container as FrameworkElement;

    // gets the parent ListBox of the item
    var parentListBox = GetParentByType(element, typeof(ListBox));

    // gets the ItemsSource from the parent ListBox
    var itemsSource = (parentListBox as ListBox).ItemsSource.OfType<Person>();

    // gets the last item in the ItemsSource
    var lastPerson = itemsSource.Last();

    if (item != null && item is Person)
    {
        Person person = item as Person;

        // checks to see if the current item is the same as the last item
        //    in the ListBox.  If so, set the DataTemplate to show the button.
        //    If not, show the template with the person's name
        if (person == lastPerson)
            return
                element.FindResource("LastRowTemplate") as DataTemplate;
        else
            return
                element.FindResource("PersonTemplate") as DataTemplate;
    }

    return null;
}

I commented the code, but I still wanted to run through the logic. Basically, I am going to get the last item in the ItemsSource and compare it to the current item. If they are the same(using our Equals method), then I will use the LastRowTemplate instead of the PersonTemplate.

The container that is passed to the SelectTemplate method is the ContentPresenter. To set to the parent ListBox, we need to walk up the visual tree. To do this, we can use the VisualTreeHelper.GetParent method. Since there are a number of controls between the ContentPresenter and the parent ListBox, I used a recursive method to get the parent. The method accepts the child element and the Type of the parent element that you are looking for. The method will walk up the visual tree until it either comes to the parent you are looking for, or when there is no parent for the child element.

private DependencyObject GetParentByType(DependencyObject element, Type type)
{
    // method will continue to loop through until it finds the
    //  parent type or no parent is found.

    // no parent was found
    if (element == null)
        return null;

    // if the types match, return it as the parent
    if (element.GetType() == type)
        return element;

    return GetParentByType(VisualTreeHelper.GetParent(element), type);
}

Next, we will turn to our XAML. In our Window.Resources section, I create a key for our DataTemplateSelector. I also create the two DataTemplates that we will need for our ListBox.

<Window x:Class="WpfApplication6.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication6"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>

        <local:ListBoxDataTemplateSelector
            x:Key="dataTemplateSelector" />

        <DataTemplate x:Key="PersonTemplate">
            <TextBlock
                Text="{Binding FullName}" />
        </DataTemplate>

        <DataTemplate
            x:Key="LastRowTemplate">
            <Button
                Content="Last Row Button"
                Click="Button_Click" />
        </DataTemplate>

    </Window.Resources>
</Window>

As you can see, the DataTemplates are also very simple(I like to keep things simple). The PersonTemplate is nothing more than a TextBlock that binds to the FullName property of the item. The LastRowTemplate is simply a Button with a Click event.

Now for our ListBox

<ListBox
    ItemTemplateSelector="{Binding Source={StaticResource dataTemplateSelector}}"
    ItemsSource="{Binding}"
    Height="211"
    HorizontalAlignment="Left"
    Margin="104,70,0,0"
    Name="peopleListBox"
    VerticalAlignment="Top"
    Width="172" />

Notice that we are binding the ItemsSource property. Also, we are binding the ItemTemplateSelector to our custom selector.

Last, but not least, our very small code-behind for the Window.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        ObservableCollection<Person> people = new ObservableCollection<Person>();
        people.Add(new Person() { FullName = "John Doe" });
        people.Add(new Person() { FullName = "Jane Doe" });
        people.Add(new Person() { FullName = "Ben Franklin" });
        people.Add(new Person() { FullName = "John Adams" });
        people.Add(new Person() { FullName = "" });

        peopleListBox.ItemsSource = people;
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Hello from the last row");
    }
}

Here, I simply create an ObservableCollection of my Person class and set it to the ItemsSource. We also have our Click event for the button here.

So if you run the application, you should see this..

Tags: , , ,

.Net, Silverlight Silverlight 4 – Custom Busy Indicator Using PathListBox

2 Comments

In a recent Silverlight addition I was making for a website, I wanted to make a busy indicator to show the user something was happening while I was off making web service calls.  To make the busy indicator, I decided to take advantage of the new PathListBox control in Expression Blend 4(and SL 4).

We are going to make a busy indicator that will rotate in a circle indefinitely, and it will look like this..

So first, let’s create a new project in Expression Blend 4.  We are just going to use the Silverlight Application project template without a website.

Next, let’s resize the window to 400×400 just to make the window a little smaller.

To create the base of our busy indicator, let’s draw a ellipse on the window, with a size of 30×30.

Now we need to put a PathListBox control on the window.  Go to the Assets tab, then start typing PathListBox in the search box.  Once it is found, simply drag it onto the window.  It doesn’t really matter where it is placed.  I placed mine at the bottom left corner of the window.

By default, the control doesn’t have a border.  If it seems to “disappear”, you can always select it from the Objects & Timeline window.

So now, we need to turn our ellipse into a Path.  This can be done simply by right-clicking on the Ellipse, Path –> Convert To Path.  You will notice that the ellipse is now a Path type.

Now that our Ellipse is a Path, we can now set it as a Layout Path for the PathListBox.  Click on the PathListBox, then go to the Properties Window.  You will see a section for Layout Paths.  Click the round circle icon(arrow in second image), and select the Ellipse.  You will see that it’s now added as a layout path.

Now we are going to add a new Ellipse as a child to the PathListBox.

<ec:PathListBox
    HorizontalAlignment="Left"
    Height="100"
    Margin="51.333,0,0,12"
    VerticalAlignment="Bottom"
    Width="100">

    <ec:PathListBox.LayoutPaths>

        <ec:LayoutPath
            SourceElement="{Binding ElementName=path}" />

    </ec:PathListBox.LayoutPaths>

    <Ellipse
        Fill="Black"
        Height="7"
        Width="7"
        Stroke="Black" />

</ec:PathListBox>

Our Ellipse is simple, with a Height and Width of 7 and a Stroke and Fill of Black.

Now that we have one Ellipse, let’s copy and paste that Ellipse to create 10 total Ellipses.  You can copy and paste the XAML, or you can use the Objects and Timeline window to copy the ellipse object.  The XAML should look like this now…

<ec:PathListBox
    HorizontalAlignment="Left"
    Height="100"
    Margin="51.333,0,0,12"
    VerticalAlignment="Bottom"
    Width="100">

    <ec:PathListBox.LayoutPaths>

        <ec:LayoutPath
            SourceElement="{Binding ElementName=path}" />

    </ec:PathListBox.LayoutPaths>

    <Ellipse Fill="Black" Height="7" Width="7" Stroke="Black" />
    <Ellipse Fill="Black" Height="7" Width="7" Stroke="Black" />
    <Ellipse Fill="Black" Height="7" Width="7" Stroke="Black" />
    <Ellipse Fill="Black" Height="7" Width="7" Stroke="Black" />
    <Ellipse Fill="Black" Height="7" Width="7" Stroke="Black" />
    <Ellipse Fill="Black" Height="7" Width="7" Stroke="Black" />
    <Ellipse Fill="Black" Height="7" Width="7" Stroke="Black" />
    <Ellipse Fill="Black" Height="7" Width="7" Stroke="Black" />
    <Ellipse Fill="Black" Height="7" Width="7" Stroke="Black" />
    <Ellipse Fill="Black" Height="7" Width="7" Stroke="Black" />

</ec:PathListBox>

Switching back to the Design view in Blend, you will now see that our Path ellipse has 5 small, black ellipses on it.

Selecting the PathListBox, we will go back to the Layout Paths section in the Properties window to change some of the options.  These options will control the number of ellipses that are visible on the Path, the padding between the ellipses, object orientation to the path, and a few other options.

Here are the options that we need to set..

So our first option is the Distribution option.  This option allows you to specify whether the path should have all of the ellipses arranged evenly on the path(Even option), or whether the path should have all of the ellipses on the path using the provided Padding option to determine the padding between the ellipses(Padded option).  Since I have chosen the Even option, Blend will automatically set the Capacity to the maximum number of ellipses that I have defined(10).  Also, the Padding option(which is set to 10 by default) will be ignored.

The second option is the Capacity option.  This option allows you to specify how many ellipses should be on the path, up to the number of ellipses defined.   Since I want to use all of the ellipses we have defined, I will set this to 10.

The third option is the Padding option.  This option allows you to specify the amount of space between the ellipses.  When the Even Distribution option is chosen, this option is ignored.

The fourth option is the Orientation option.  This option allows you to specify the orientation of the ellipses in relation to the path curve.  Since we are using ellipses, changing this option will have no visible effect.

The fifth option is the Start option.  This option allows you to specify the distance from the start of the path to place the first ellipse.  In our case, when the Start option is at 0, the first ellipse is on the right of the path(at 3 o’clock if you think of the path as a clock).  Since I want the first ellipse to be at top-dead center, I set the Start option to -25%.

The sixth option is the Span option.  This option allows you to specify how much of the path will actually participate in the Layout Path.  Since we want the entire path to be included, we will leave this at 100%.

The last option is the FillBehavior option.  To better explain this option, I am going to let Microsoft do the talking…

In the FillBehavior box, choose one of the following:

  • NoOverlap   If your layout path is an open path and you want to animate the Start property, select NoOverlap from theFillBehavior drop-down list. This reduces the effective span used to lay items out along the path, allowing enough room to animate Start without the items on the path overlapping when the list items return to the beginning of the layout path.
  • FullSpan   Select FullSpan from the FillBehavior drop-down list to make sure that the entire span of the layout path is used. For example, if you have selected Even in the Distribution drop-down list and FullSpan in the FillBehavior drop-down list when working with an open path, an item will appear at the beginning and at the end of the path.

Our next task is to offset the colors of each of the ellipses to give them a “fading” effect.  To do this, select the ellipse directly to the left of the top-dead center ellipse, then in the Properties Window, set the Alpha value of the Fill color to 90%.

Do this for each of the ellipses as you move around the circle counter-clockwise.  For each ellipse, set the Alpha value to 10% less than the previous ellipse.  So our next ellipse would be set to 80%.

After you have completed, the last ellipse will have an Alpha value of 10%, and the path will now look like this…

Now, let’s make the borders disappear.  Select all of the ellipses INCLUDING the path ellipse, and in the Properties Window, set the Stroke to No Brush.

Now deselect all of the ellipses and ONLY select the Path ellipse, then in the Properties Window, set the Fill to No Brush.

So we have now completed the visual part of making the our busy indicator.  Next, we need to do an animation for the path to rotate.

From the Blend Designer, go to Window –> Workspaces –> Animation, or hit F6.  Now that we are in animation view, we will now create a new storyboard for our animation.  In the Objects and Timeline window, click the plus icon to create a new storyboard.

I will name it RotateStoryboard.

Once the storyboard is created, Blend will automatically to into recording mode.  Select the ellipse path, then click the Record Keyframe icon to record a keyframe of the current properties.

Now that we have our first keyframe, we want to move the timeline to 0.2 seconds(click and drag the arrow at the top of the yellow line).  With the ellipse path still selected, go to the Transform section of the Property Window, choose the Rotate tab, and set the Angle to 90.  This will add a second keyframe to the timeline.

Now move the yellow line to 0.4 seconds, and set the Angle to 180.

Now move the yellow line to 0.6 seconds, and set the Angle to 270.

Now move the yellow line to 0.8 seconds, and set the Angle to 360.

You can now hit the Play button and watch the animation start.

Now let’s stop the animation from recording by clicking the record button at the top.

Now we need to make the animation run forever.  To do this, we will select the storyboard by clicking it in the Objects and Timeline window.  In the Properties Window, we will set the RepeatBehavior property to Forever.

Now we need to make the storyboard start when the control is loaded.  To do that, we will use a behavior.  Specifically, we will use the ControlStoryboardAction behavior.  We will switch back to the design view by hitting F6 or Window –> Workspaces –> Design.

In the Assets window, we will choose Behaviors, then select ControlStoryboardAction(make sure you have cleared the search box which may still have PathListBox in it).

We will drag this behavior and drop it on the UserControl.

In the Properties Window, set the EventName to Loaded, and set the Storyboard to RotationStoryboard.  This will play the storyboard when the UserControl has been loaded.

And we are done.  Hit F5 to run the application, and behold your custom, beautiful busy indicator.

Tags: , ,

.Net, Windows Phone 7 WP7 – Using IsolatedStorageSettings

8 Comments

In a previous tutorial, I wrote about using the Isolated Storage to store files on a WP7 device. Let’s say that instead of storing data, you wanted to store settings for your application. This where the IsolatedStorageSettings comes in.

The IsolatedStorageSettings is a place to store Key/Value pairs of data.

So let’s say that my application used the Microsoft Ad control, but I wanted to be nice and give the user the ability to turn the ads on and off.

On my Settings screen, I would have a checkbox(AdsCheckbox) for allowing the ads. In code, I would save the value by doing this…

var settings = IsolatedStorageSettings.ApplicationSettings;

if (settings.Contains("AllowAds"))
    settings["AllowAds"] = AdsCheckbox.IsChecked.Value;
else
    settings.Add("AllowAds", AdsCheckbox.IsChecked.Value);

settings.Save();

If you will notice, the ApplicationSettings object does have a method that allows you to check to see if the key already exists in the Settings. An exception will be thrown if you try to add a key that already exists, so it’s necessary to do this check before assigning a value.

Also, as you can see, it’s very easy to get the value out of the settings…

// when the form opens, I want to set the Checkbox to being checked
//   or unchecked depending on the setting
var settings = IsolatedStorageSettings.ApplicationSettings;

if (settings.Contains("AllowAds"))
    AdsCheckbox.IsChecked = bool.Parse(settings["AllowAds"].ToString());

However, the IsolatedStorageSettings class isn’t only for simply datatypes. It can also store your custom class objects.

Employee class

public class Employee
{
    public string FullName { get; set; }
    public decimal Salary { get; set; }
}

Then on my form, I have a button to save an instance of the Employee class to the IsolatedStorageSettings

private void btnSave_Click(object sender, RoutedEventArgs e)
{
    var settings = IsolatedStorageSettings.ApplicationSettings;

    Employee emp = new Employee()
    {
        FullName = "John Doe",
        Salary = 250000
    };

    settings.Add("Employee1", emp);
    settings.Save();
}

I also have a button that will load the object from the IsolatedStorageSettings

private void btnLoad_Click(object sender, RoutedEventArgs e)
{
    var settings = IsolatedStorageSettings.ApplicationSettings;

    Employee emp;

    if (settings.TryGetValue<Employee>("Employee1", out emp))
    {
         MessageBox.Show(string.Format("Full Name: {0}\nSalary: {1:c}", emp.FullName, emp.Salary));
    }
}

Notice that the IsolatedStorageSettings.ApplicationSettings class also has the TryGetValue method. This method behaves exactly like the TryParse methods of the .Net datatypes. It will return a boolean specifying whether the key was found and whether the conversion from object –> Employee was successful. If it’s successful, it will return the instance as the out parameter.

After running the application, clicking the Save button, then clicking the Load button, we will get this message box…

2

Tags: , , , , ,