Silverlight toolkit: Problem with the ViewBox, named elements, and a workaround

.NET, Silverlight, Technical stuff, Work
See comments

In the latest edition of the Silverlight toolkit (December 2008), there is a bug with the ViewBox control. First a reminder: A ViewBox is an element used to zoom a scene. For example, if the ViewBox’ content has a dimension of 800×600, and the ViewBox itself has a dimension of 400×300, the content will be automatically zoomed. For example:

image

Original content in Blend

image

Smaller Content in IE

image

Bigger content in IE

This is a very handy control, because it allows to design against a give width and height (for example 1024×768) and then the application will automatically scale itself to fill the available space given by the DIV element containing it.

However, in the current version of this control, an issue makes that named element within the ViewBox (or within the ViewBox’s children) will not be set correctly. So if we have:

<controls:Viewbox x:Name="MyViewBox"
                  VerticalAlignment="Top">

  <Grid x:Name="LayoutRoot"
        Background="Red"
        Height="240"
        Width="320">

    <TextBlock x:Name="MyTextBlock"
               Margin="15,15,0,0"
               VerticalAlignment="Top"
               Text="TextBlock"
               FontFamily="./Fonts/Fonts.zip#Algerian"
               FontSize="22"
               Foreground="#FFFFFFFF" />

  </Grid>
</controls:Viewbox>

Then the following calls all return null:

TextBlock myTextBlock = MyTextBlock;      // returns null

myTextBlock
  = FindName("MyTextBlock") as TextBlock; // returns null

Thankfully it’s not all too difficult to correct this issue. We will create extension methods which should cover all possible cases. The methods will loop through all the children of the given element, and check the element’s name. If the name exists, and is the one we’re looking for, we will return it.

As for all extension methods, they must be placed in a static class. The best is to create a new file in the same project (or in a referenced assembly) and to copy the following code:

public static class TryFindNameExtensions
{
  public static UIElement TryFindName(this UIElement parent, 
    string name)
  {
    if (parent is Panel)
    {
      return (parent as Panel).TryFindName(name);
    }
    if (parent is Border)
    {
      return (parent as Border).TryFindName(name);
    }
    if (parent is ContentControl)
    {
      return (parent as ContentControl).TryFindName(name);
    }

    return null;
  }

  public static UIElement TryFindName(this Panel parent, 
    string name)
  {
    foreach (UIElement el in parent.Children)
    {
      if (el.GetValue(FrameworkElement.NameProperty).ToString() 
        == name)
      {
        return el;
      }
    }

    // Not found
    foreach (UIElement element in parent.Children)
    {
      UIElement foundElement = element.TryFindName(name);
      if (foundElement != null)
      {
        return foundElement;
      }
    }

    return null;
  }

  public static UIElement TryFindName(this Border parent, 
    string name)
  {
    if (parent.Child.GetValue(FrameworkElement.NameProperty)
    .ToString() == name)
    {
      return parent.Child;
    }

    return parent.Child.TryFindName(name);
  }

  public static UIElement TryFindName(this ContentControl parent, 
    string name)
  {
    UIElement el = parent.Content as UIElement;

    if (el != null
      && el.GetValue(FrameworkElement.NameProperty)
      .ToString() == name)
    {
      return el;
    }

    return el.TryFindName(name);
  }
}

The first method finds an element by name in a UIElement. Silverlight has 3 elements that can contain other elements:

  • Panels (and derived classes such as Grid, StackPanel, etc…) have a Children collection. Each child is a UIElement.
  • Border has a Child element of type UIElement.
  • ContentControl (and all derived elements such as Button, UserControl, etc…) has a Content property of type object.

Depending on the element’s type, the first method calls the corresponding extension method.

  • For Panels, we loop through all the Children. If we find the corresponding name, we return the child. If we don’t, we call the method recursively on each child, until we can find the element we look for. If we don’t find anything, we return null.
  • For Borders, we do the same but there is only one child, so it’s easier.
  • For ContentControls, we check first if the Content is a UIElement. If yes, we executed the same search.

Finally, in the Page constructor, we assign the found element to the corresponding identifier. Note that the identifier (for example MyTextBlock) is already declared, because the element is named in the XAML code. The compiler creates a partial class containing all the named elements. Because of the bug, the element is assigned to null, but we can set it in our code:

public Page()
{
  InitializeComponent();

  if (this.MyViewBox != null)
  {
    Panel layoutRoot = this.MyViewBox.Child as Panel;
    MyTextBlock
      = layoutRoot.TryFindName("MyTextBlock") as TextBlock;
  }
}

Now we can do:

TextBlock myTextBlock = MyTextBlock;      // not null anymore
myTextBlock
  = FindName("MyTextBlock") as TextBlock; // still returns null

When will we get a fix?

According to what I heard, the bug is already fixed, but the exact date of release is not known yet. In the mean time, you can use the fix above. While a bit cumbersome, it will however continue to work even after the bug is fixed.

Previous entry | Next blog entry

Comments for Silverlight toolkit: Problem with the ViewBox, named elements, and a workaround