Showing posts with label Layout. Show all posts
Showing posts with label Layout. Show all posts

Wednesday, 10 March 2010

Flexibox – A Silverlight alternative to Lightbox

Introduction

Flexibox is an alternative to lightbox, displaying multiple resolutions of an image without needing a popup overlay. Flexibox shows how a Silverlight app can resize itself with a page.

Great photos need to displayed big to be best appreciated, but smaller image sizes work best within a block of text, or as a collection of thumbnails. To solve this design dilemma thumbnail images often contain a link to view a larger version of the same image. If this takes the user to another page, then this is inconvenient as the user has to go back to the original page to continue.

A popular alternative is to use a JavaScript library, such as Lightbox, to display an overlay image on the current page. This is very impressive the first few times one uses it. However the overlay hides the rest of the page. The distraction and time taken acts as a disincentive for users to see the larger version of the image.

Flexibox displays a single image within a HTML page in the same way as an <img> tag. Flexibox can change the image it displays to one of a different size and automatically resizes itself within the page. This enables a user to go from a thumbnail to a large resolution image without needing to leave the page.

The Flexibox appears on a web page just like any thumbnail image, but has an enlarge button overlaid on the top right to indicate to the user there is a larger version.

When the user clicks on the enlarge button the Flexibox is enlarged instantly causing the surrounding page elements to re-formatted to flow around the now larger control.

To see a Flexibox live go to http://ithinkly.com/demo/flexibox/

 

The Code

The code demonstrates a number of useful Silverlight techniques:
Passing parameters from the HTML page to the Silverlight
Converting CSS color strings into Silverlight Color structures
Loading content relative to the host page
Asynchronous loading and caching of images
Resizing a Silverlight control within a page

For what seems to be a very simple application Flexibox contains a lot more code than can be covered here, so only the techniques listed above are described. The full source code is  provided, so you can study all the details in full.
The project was developed with VS 2010 targeting Silverlight 3.
The HTML

The first thing to study is the way the Flexibox control HTML differs from the standard.

<div style="margin-right: 8px; margin-bottom: 3px;float: left;">
        <object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="240px" height="161px">
  <param name="source" value="ClientBin/FlexiBox.xap"/>
  <param name="onError" value="onSilverlightError" />
  <param name="background" value="white" />
  <param name="minRuntimeVersion" value="3.0.40818.0" />
  <param name="autoUpgrade" value="true" />
  <param name="initParams" value="border=2,border_color=#444,preload=true,mode=small,thumb_url=images/s1_100.jpg,medium_url=images/s1_500.jpg,large_url=images/s1_1024.jpg"  />
  <a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=3.0.40818.0" style="text-decoration:none">
               <img src="http://go.microsoft.com/fwlink/?LinkId=161376" alt="Get Microsoft Silverlight" style="border-style:none"/>
  </a>
</object><iframe id="_sl_historyFrame" style="visibility:hidden;height:0px;width:0px;border:0px"></iframe></div>


The object tag is given a fixed width and height in pixels. These attributes will be later changed from within Silverlight.

The div tag surrounding the object tag uses an in-line style to set the margin and float the control to the left of the surrounding text. You can use almost any style in here, except for  width or height. Also do not set an id or class style for the div tag, as the default Visual Studio generated page will do.



The initParams param tag contains the parameters to be passed to the Silverlight control. The example above sets the width and color of the border and passes in four images. The image URLs are relative to the page, just as one would use in an img tag. The mode is set to small so that the small image will be the first one displayed.



The Flexibox XAML






There is only one control in the application and that is defined as follows.



<Border BorderBrush="{Binding Path=BorderBrush}" BorderThickness="{Binding Path=BorderThickness}" Name="border1"  >
    <Grid x:Name="LayoutRoot" >
        <Image  Name="image1" Stretch="None" Source="{Binding Path=DisplayedImage}" />
        <controlsToolkit:BusyIndicator Height="59" HorizontalAlignment="Center"  Name="busyIndicator1" VerticalAlignment="Center" Width="151" IsBusy="{Binding Path=IsBusy}" BusyContent="{Binding Path=BusyStatus}" DisplayAfter="00:00:02.1000000" IsEnabled="True" />
        <StackPanel Height="25" HorizontalAlignment="Right" Margin="0,2,2,0" Name="stackPanelButtons" VerticalAlignment="Top"  Orientation="Horizontal">
            <Button Style="{StaticResource ButtonIcon}" Click="Contract_Button_Click" Name="ContractButton" RenderTransform="{StaticResource ButtonBottomLeft}" Visibility="{Binding Path=ContractVisible}" />
            <Button Style="{StaticResource ButtonIcon}" Click="Expand_Button_Click" Name="ExpandButton"  RenderTransform="{StaticResource ButtonTopRight}" Visibility="{Binding Path=ExpandVisible}" />
        </StackPanel>
    </Grid>
</Border>


As you would expect there is a Border, an Image control and a Busy Indicator for when  loading the first image.

The StackPanel is used to align the two buttons, which only become visible when they can be used. The buttons are simply re-styled. See the styles.xaml file for details.


All the dynamic elements are controlled by data binding to properties of the model.



The Code behind file



public partial class MainPage : UserControl
{
    FlexiBoxViewModel viewModel = new FlexiBoxViewModel();
    public MainPage()
    {
        InitializeComponent();
        DataContext = viewModel;
    }
    private void Contract_Button_Click(object sender, RoutedEventArgs e)
    {
        viewModel.Contract();
    }
    private void Expand_Button_Click(object sender, RoutedEventArgs e)
    {
        viewModel.Expand();
    }
}


As can be seen all the interesting stuff is in the viewModel. Handlers for the button clicks are required as we are using Silverlight 3. In 4 we can get rid of these and use commands directly within the XAML.



The App.xaml.cs file contains only the generated code, so no need to look at that.



Reading the parameters from the HTML page



foreach (string key in Application.Current.Host.InitParams.Keys)
    ParseExternalParam(key, Application.Current.Host.InitParams[key]);


The Application Host contains a dictionary of all the parameters passed into the control, which can be processed one by one.



private void ParseExternalParam(string key, string value)
{
    try
    {
    ...
        else if (String.Compare("thumb_url", key, StringComparison.InvariantCultureIgnoreCase) == 0)
        {
            images[(int)FlexiImageModel.Size.thumb].Url = value;
        }
        else if (String.Compare("border", key, StringComparison.InvariantCultureIgnoreCase) == 0)
        {
            borderSize = Double.Parse(value);
        }
        else if (String.Compare("border_color", key, StringComparison.InvariantCultureIgnoreCase) == 0)
        {
            borderColor = value.ToColor();
        }
    }
    catch
    {
        // contain any exceptions thrown by invalid params
    }
}


Each parameter key is checked for a match and then the value is converted into the appropriate type and stored for later use.

As elsewhere in the code exceptions are caught and safely ignored.   If this is not done then entering an invalid number for the border parameter would cause a nasty error message to be displayed to the user.


This may be what you want, in which case remove the try catches, but I preferred the app to fail quietly and continue in the same way XAML does when it finds errors.



Unlike the full version of .Net there is no built in way of converting a string representation of a color into a color object in Silverlight.

Many people have had to tackle this in the last two years and there are plenty of examples on the web, but most just do hex strings in one format.


I wanted a color string converter that could handle all the CSS color formats. e.g. '#FFFFFF', '#FFF', 'white'.



So I wrote the following utility class.



public static class TWSilverlightUtilities
{
    // Converts a CSS style string into Color structure. Returns black if the input is invalid.
    // The color string to convert. May be in one of the following formats; #FFFFFFFF, #FFFFFF, #FFF, white.
    public static Color ToColor(this string str)
    {
        Color rv = Color.FromArgb(0xFF,0,0,0);
        try
        {
            rv = str.ToColorEx();
        }
        catch
        {
        }
        return rv;
    }
    public static Color ToColorEx(this string str)
    {
        // empty string check
        if ((str == null) || (str.Length == 0))
            throw new Exception("empty color string");
        // This is the only way to access the colors that XAML knows about!
        String xamlString = "<Canvas xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" Background=\"" + str + "\"/>";
        Canvas c = (Canvas)System.Windows.Markup.XamlReader.Load(xamlString);
        SolidColorBrush brush = (SolidColorBrush)c.Background;
        return brush.Color;
    }
}


I really wanted to write this as an extension method of Color, as it would then match the ToString method nicely.

Unfortunately Color is a struct and extension methods do not work properly on them as the 'this' parameter can only be passed by value and not by reference, which makes it useless for structs. Also you cannot define a static extension method, only instance one. Both serious omissions in the language in my view.



Loading content relative to the host page






The URLs of the images to display could be given as absolute or relative to the web page. The Silverlight control may be hosted in a completely different location so the correct address must be used, which is obtained from HtmlPage.Document.DocumentUri. Its then a matter of stripping off the page name, query and fragment and appending the relative image URL, to give us the absolute URL of the image that is required.



private Uri GetImageUri(FlexiImageModel.Size size)
{
    Uri imgUri = new Uri(images[(int)size].Url, UriKind.RelativeOrAbsolute);
    if (imgUri.IsAbsoluteUri)
        return imgUri;
    // Make an absolute URL relative to the document we are in
    UriBuilder pageUri = new UriBuilder(HtmlPage.Document.DocumentUri);
    pageUri.Fragment = null;
    pageUri.Query = null;
    int n = pageUri.Path.LastIndexOf('/');
    if (n > 0)
        pageUri.Path = pageUri.Path.Substring(0, n + 1);
    return new Uri(pageUri.Uri, imgUri);
}


Asynchronous loading and caching of images



Each Flexibox can hold up to four images; thumb, small, medium and large. These can either be loaded one at a time on  request, or all pre-loaded.

The pre-load feature gives the user instant access to the different sized images, but uses more band width, so each option has its uses.


When pre-loading the most important thing is to load the image to be initially shown first. Only when that is download should the other images be downloaded and cached.


It is possible that the user may click on the expand button while the next image is being downloaded and the code needs to cope with that.


The caching of the images is plain C# and not a Silverlight technique, so it is not explained in detail here, but is in the attached source code.



private void RetrieveImage(FlexiImageModel.Size size)
{
    try
    {
        // Dont start a second retrieve of the same image
        if (!images[(int)size].IsRetrieving)
        {
            Uri uri = GetImageUri(size);
            images[(int)size].IsRetrieving = true;
            WebClient webClient = new WebClient();
            webClient.OpenReadCompleted += new OpenReadCompletedEventHandler(webClient_OpenReadCompleted);
            webClient.OpenReadAsync(uri, size);
        }
    }
    catch (Exception ex)
    {
        images[(int)size].IsError = true;
    }
}


To download an image firstly get its full URL as described above and then use a WebClient to start the download asynchronously, so as not to block the user interface.

When starting the operation we pass in the size of the image, so we know which image has been downloaded when it completes.



void webClient_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
    FlexiImageModel.Size size = newSizeMode;
    if (e.UserState is FlexiImageModel.Size)
        size = (FlexiImageModel.Size)e.UserState;
    try
    {
        if ((e.Cancelled) || (e.Error != null))
            throw new Exception("Error downloading image");
        BitmapImage bitmap = new BitmapImage();
        bitmap.SetSource(e.Result);
        e.Result.Close();
        images[(int)size].Bitmap = bitmap;
        images[(int)size].IsError = false;
    }
    catch
    {
        images[(int)size].IsError = true;
        BusyStatus = "Error loading image";
    }
    images[(int)size].IsRetrieving = false;
    LoadImage();
    RetrieveNextImage();
}


WebClient has several options on how to receive the downloaded data. By using OpenReadAsync it provides an open stream in e.Result, which is perfect to set as the source for the new BitmapImage object.

Of course things can go wrong and any errors are caught and the image marked as an error in the cache.



Once the image has been cached Flexibox is ready to load it into the user control and then start the download of the next image.



Loading an image into the user interface



private void LoadImage()
{
    if ( ((imageLoaded) && (sizeMode == newSizeMode)) ||
        (images[(int)newSizeMode].IsError) || (!images[(int)newSizeMode].IsLoaded))
        return; // nothing to do
    sizeMode = newSizeMode;
    imageLoaded = true;
    ContractVisible = (sizeMode != NextSmallerImage(sizeMode)) ? Visibility.Visible : Visibility.Collapsed;
    ExpandVisible = (sizeMode != NextBiggerImage(sizeMode)) ? Visibility.Visible : Visibility.Collapsed;
    ResizeControl();
    NotifyPropertyChanged("DisplayedImage");
    IsBusy = false;
}


 



The method first checks for errors and if the required image is already loaded, before proceeding with the rest of the operation.



The Expand and Contract buttons need to be turned on or off depending on whether there is a bigger or smaller image available.



public Visibility ExpandVisible { get { return expandVisible; } set { expandVisible = value; NotifyPropertyChanged("ExpandVisible"); } }
public Visibility ContractVisible { get { return contractVisible; } set { contractVisible = value; NotifyPropertyChanged("ContractVisible"); } }


This is done by setting the two public properties that the XAML defined buttons are bound to.



public BitmapImage DisplayedImage { get { return images[(int)sizeMode].Bitmap; } }


The XAML defined image is bound to the  DisplayedImage property, which retrieves the image directly from the cache. However the image will not be updated as it needs to be told that the selected image has changed. This is done with the NotifyPropertyChanged call at the exact time required, which is after the control has first been resized to fit the new image.



Resizing a Silverlight control within a page






Now for the final technique and the one that this control is really all about, resizing a Silverlight control within a page.

I want to give full credit for this technique to Charles Petzold.



private void ResizeControl()
{
    if (!images[(int)sizeMode].IsLoaded)
        return;
    double height = Math.Max(20,(2 * borderSize) + images[(int)sizeMode].Bitmap.PixelHeight);
    double width = Math.Max(20,(2 * borderSize) + images[(int)sizeMode].Bitmap.PixelWidth);
    if (controllWidth != width)
    {
        controllWidth = width;
        SetDimensionPixelValue("width", controllWidth);
    }
    if (controllHeight != height)
    {
        controllHeight = height;
        SetDimensionPixelValue("height", controllHeight);
    }
}


Each bitmap image exposes its dimensions so we can calculate the new width and height of the control by adding these to the border thickness.

It is then a matter of setting the width and height using this method.



void SetDimensionPixelValue(string style, double value)
{
    HtmlPage.Plugin.SetAttribute(style, ((int)Math.Round(value)).ToString() + "px");
}


HtmlPage.Plugin provides access to the HtmlElement of the object tag. It is then just a case of setting either the width or the height attribute to a text definition of the measurment.

In order for this to work it is important that the page does not contain additional width and height settings that may affect the control within the page, or one of its div containers. This is done by default using styles in the generated code.



Conclusion






As well as explaining a number of basic Silverlight techniques, I am hoping that the control itself shows how Silverlight can be used as an integral part of a web page design.



Source code



Try it live

Thursday, 8 October 2009

WPF Collapse Converters for easier data layouts

Introduction

This article demonstrates how WPF Converters can be used in an innovate way to replace the use of  WPF Triggers in a common user interface task. The code contains a small set of self contained classes that can be easily added to any WPF project.

Background

A common design issue when presenting complex data is how much detail to display.  Too much detail and the user may get confused or find it more difficult to find the item that they are looking for. Too little and the user may not have all the information that they need. In the past many of these decisions were fixed at design time. Modern user interfaces dynamically hide and display additional data through the users actions.
This example uses a product list from a fictional bike company.
Products have a code, name, price, description, optional photo and a sale ended date.
The data is taken from the SQL Server Adventure Works sample database. All the data is contained in the code so there is no need to use an external database to run the example code.
The example application consists of a single window containing an ordinary list box filled with a list of objects from the Product class. A WPF DataTemplate is used to format each product in the list. This sort of layout can be found in many WPF samples and tutorials. What makes it more interesting is the checkboxes on the window that dynamically control how much information is displayed.
In the second screenshot the images and prices have been hidden and only the currently selected product has its description shown. There are additional automatic features as well. If an item has a blank Sale Ended date then the red line at the bottom is hidden and if an image is not available the image and its border are hidden.
The usual way of doing this
The data template contains contains controls to display each part of the product. Each UIElement  has a Visibility Property and setting that to the value Collapsed hides the element and forces its container to layout the other controls as if it was not there.  So all that is needed is a means for setting the Visibility property under the right circumstances.
The usual way to do this is with a DataTrigger, as show in this code example.
<TextBlock Margin="0,3,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Width="Auto" Height="Auto" TextWrapping="Wrap" FontStyle="Italic" TextDecorations="None"
Grid.Column="1" Grid.ColumnSpan="1" Grid.Row="1" TextAlignment="Left" Text="{Binding Path=Description}" TextTrimming="WordEllipsis" FontSize="10" >
<TextBlock.Style>
<Style>
<Style.Triggers>
<DataTrigger
Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem} }, Path=IsSelected}"
Value="False">
<Setter Property="TextBlock.Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>



This is essentially a giant if statement that says if the parent item is not selected then hide the description. I do not like triggers because they add programming logic to the user interface, which is not a good thing. They are also over complicated, the trigger code above takes no account of the 'Always Display Descriptions' checkbox, to do so would need an even more complicated definition.


The Collapse Converters


The great thing about WPF is that there are usually several ways to achieve the same thing so I came up with an alternative way that uses converters instead of triggers.


Converters are used to convert one value type into another type during data binding. A classic case contained in the example code implements a converter that formats a C# DateTime into a string for display as the Sell End Data. Converters are C# classes that implement the IvalueConverter interface.


Before describing the collapse converters converters in detail I will show you how they are used.


public partial class CollapseConverterWindow : Window
{
public CollapseConverterWindow()
{
InitializeComponent();
listBoxProducts.ItemsSource = TWSampleWPFDatabase.SampleData.Instance.Products.Items;
}
} 


The code behind the example window is very minimal the only thing it does is initialize the Window and set the listbox's contents to an ObservableCollection of Product objects.  The product class and the same data is all contained in a separate sub-project that may be worth an article in itself, but all we need to know here is that the Product class has public properties for each item that we want to display.


<Window x:Class="TWCollapseConverters.CollapseConverterWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TWCollapseConverters"
Title="Collapse Converter Example" Height="419" Width="396" Loaded="Window_Loaded">
<Window.Resources>
<local:CollapseOnBlankConverter x:Key="collapseOnBlankConverter"/>
<local:CollapseOnBlanksConverter x:Key="collapseOnBlanksConverter"/>
<local:CollapseOnBlanksFirstPairOrConverter x:Key="collapseOnBlanksFirstPairOrConverter"/>
<local:DateToShortDateStringConverter x:Key="dateToShortDateStringConverter"/>
<DataTemplate x:Key="ProductTemplate">
.....
</DataTemplate>
</Window.Resources>
<Grid Background="LightGray">
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="3" HorizontalAlignment="Center">
<CheckBox Height="16" Name="checkBox_AlwaysDisplayDescriptions" IsChecked="True">Always Display Descriptions</CheckBox>
<CheckBox Height="16" Margin="10,0,0,0" Name="checkBox_DisplayImages" IsChecked="True">Display Images</CheckBox>
<CheckBox Height="16" Margin="10,0,0,0" Name="checkBox_DisplayPrices" IsChecked="True">Display Prices</CheckBox>
</StackPanel>
<ListBox Grid.Row="1" Name="listBoxProducts"  ItemTemplate="{StaticResource ProductTemplate}" HorizontalContentAlignment="Stretch" />
</Grid>
</Window> 


The XAML window definition follows a simple layout, with the complexity contained in the DataTemplate. Instances of the converters are defined in the Windows.Resources section and then referenced in the DataTemplate.


Instead of listing the entire data template the parts that demonstrate each of the converts is shown below.

CollapseOnBlankConverter


The simplest of the three collapse converters takes a single value and converts it to either Visible or Collapse.


<StackPanel  Orientation="Horizontal" Visibility="{Binding ElementName=checkBox_DisplayPrices, Path=IsChecked, Converter={StaticResource collapseOnBlankConverter}}" Grid.ColumnSpan="2" HorizontalAlignment="Left">
<TextBlock Text="Price: $" Margin="4,0,0,0" Foreground="DarkGreen" FontWeight="Bold" />
<TextBlock Text="{Binding Path=ListPrice}" Margin="0,0,2,0" Foreground="DarkGreen" FontWeight="Bold" />
</StackPanel> 


In this example visibility of the container for the Price text is bound to the value of the 'Display Prices' checkbox. The IsChecked property returns a boolean value which cannot be directly bound to a visibility field, but by specifying collapseOnBlankConverter  as the Converter the check value will be converted into Visible or Collapse.


<StackPanel Grid.Column="1" Grid.Row="2" Orientation="Horizontal" Visibility="{Binding Path=SellEndDate, Converter={StaticResource collapseOnBlankConverter}}">
<TextBlock Text="Sale Ended On: " Margin="0,3,3,0" Foreground="DarkRed" FontWeight="Bold" />
<TextBlock Text="{Binding Path=SellEndDate, Converter={StaticResource dateToShortDateStringConverter}}" Margin="0,3,0,0" Foreground="DarkRed" FontWeight="Bold" />
</StackPanel> 


In the second example the visibility is bound to the value of the date and this too is converted into Visible or Collapse.

How the Converter interprets its input gives rise to its name. If a value can be considered blank then it will return Collapse otherwise it returns Visible. So a boolean false, a null reference, an empty string, a blank date, or the number zero are all considered blank and so cause the element to be hidden.

CollapseOnBlanksConverter


The second collapse converter takes a list of inputs instead on a single input. Defining a multiple binding cannot be done via the inline notation in the previous example. Instead it has to be defined the verbose way.


<Border Margin="5" BorderThickness="1" BorderBrush="DarkGray" HorizontalAlignment="Center" VerticalAlignment="Top">
<Border.Visibility>
<MultiBinding  Converter="{StaticResource collapseOnBlanksConverter}">
<MultiBinding.Bindings>
<Binding ElementName="checkBox_DisplayImages" Path="IsChecked"  />
<Binding Path="ThumbNailPhotoAvailable"  />
</MultiBinding.Bindings>
</MultiBinding>
</Border.Visibility>
<Image  Source="{Binding Path=ThumbNailPhoto}" />
</Border> 


This example the visibility of the image border element is bound to both the 'Display Images' check box and the boolean property of the Product that indicates if an image is available.

The CollapseOnBlanksConverter checks that all its inputs and if any one is blank it will return Collapse. That way the image will be hidden if is not available or the user has turned images off with the checkbox.


There is no limit to the number of conditions that you can set in this way.


The Image element will not display anything is its source is not defined so this converter may look unnecessary. However the border will still be displayed around a blank image and anyway the Product class returns an image valid image.


CollapseOnBlanksFirstPairOrConverter


The third converter is a variation on the second in that it takes the combined value of the first two elements and compares those with the rest.

e.g. CollapseOnBlanksConverter => (A && B && C && D) and CollapseOnBlanksFirstPairOrConverter => ((A || B) && C && D)


<TextBlock Margin="0,3,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Width="Auto" Height="Auto" TextWrapping="Wrap" FontStyle="Italic" TextDecorations="None"
Grid.Column="1" Grid.ColumnSpan="1" Grid.Row="1" TextAlignment="Left" Text="{Binding Path=Description}" TextTrimming="WordEllipsis" FontSize="10" >
<TextBlock.Visibility>
<MultiBinding  Converter="{StaticResource collapseOnBlanksFirstPairOrConverter}">
<MultiBinding.Bindings>
<Binding ElementName="checkBox_AlwaysDisplayDescriptions" Path="IsChecked"  />
<Binding Path="IsSelected" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem} }"   />
<Binding Path="Description"  />
</MultiBinding.Bindings>
</MultiBinding>
</TextBlock.Visibility>
</TextBlock> 


So in this example the logic is to always hide the description if it is blank and to show it if the item is selected or if the 'Always Display Descriptions' checkbox is ticked.

This does the same job and a lot more than the Trigger example given at the top of the article and is I think a lot easier to define.


The C# code


Now you have seen how they are used all that is left is to show you the Converters code implementation.

The code is in a single C# file TWCollapseConvertersLib.cs, which is part of the main project. It could be put in its own library but its too small to bother.


CollapseOnBlanksConverter


public class CollapseOnBlankConverter : IValueConverter
{
static public bool IsBlank(object value)
{
if ((value == null) ||
((value is bool) && ((bool)value == false)) ||
((value is string) && (((string)value).Length == 0)) ||
((value is int) && (((int)value) == 0)) ||
((value is double) && (((double)value) == 0)) ||
((value is decimal) && (((decimal)value) == (decimal)0)) ||
((value is System.DateTime) && ((System.DateTime)value == System.DateTime.MinValue))
)
return true;
return false;
}
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (CollapseOnBlankConverter.IsBlank(value))
return System.Windows.Visibility.Collapsed;
return System.Windows.Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException("unexpected Convertback");
}
} 


The static IsBlank method interprets whether the input is blank or not. The value is passed in a an object so it has to determine its type and then  perform the relevant comparison. If you are using a type not listed here then its a simple matter to add a new clause to the list.


The IvalueConverter requires we define conversion going both ways. We never need to convert back so an exceptions has been added as a sanity check.


CollapseOnBlanksConverter

This converter implements the multi value interface and so loops through the given list.


public class CollapseOnBlanksConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
foreach (object v in values)
if (CollapseOnBlankConverter.IsBlank(v))
return System.Windows.Visibility.Collapsed;
return System.Windows.Visibility.Visible;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException("unexpected Convertback");
}
} 


CollapseOnBlanksFirstPairOrConverter

This converter is the most complex of the three as it has to treat the first two values differently, so the code is a little more complex.

However it is so much easier to Implement complex logic in C# than it is in XAML using Triggers.


public class CollapseOnBlanksFirstPairOrConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
for (int i = 0; i < values.Length; i++)
{
if ((i==0) && (values.Length > 1))
{
if ((CollapseOnBlankConverter.IsBlank(values[i])) && (CollapseOnBlankConverter.IsBlank(values[i+1])))
return System.Windows.Visibility.Collapsed;
i++;
}
else if (CollapseOnBlankConverter.IsBlank(values[i]))
return System.Windows.Visibility.Collapsed;
}
return System.Windows.Visibility.Visible;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException("unexpected Convertback");
}
} 


Conclusion

Hopefully this article will have shown you how WPF can be used in different ways from the standard tutorials and how a little extra C# coding can go a long way to make your XAML layout a lot easier to develop.


Download

Copyright (c) 2009 Tom Wright. All rights reserved.

This code is distributed under the Microsoft Public License (Ms-PL).


Exe file


Full Source code - VS2008