Pages
    Categories
    Archive
    Keywords

    None

      Calendar
      <<  February 2012  >>
      MoTuWeThFrSaSu
      303112345
      6789101112
      13141516171819
      20212223242526
      2728291234
      567891011
      murven , created on 16. October 2009, 04:17

      During the design of the user interface, controls are declared inside their containers using XAML. However, most of the times, the quantity of controls a container will hold is known only at runtime. Moreover, controls are usually dependant on a data source, which changes dynamically during the lifecycle of the application. For most cases, data binding would be the way to go; it is the most maintainable and scalable way of adding controls in runtime; however, developers will sooner or later have the need to add controls to a container dynamically during runtime without data binding. This article shows the basics of adding controls during runtime, as well as highlighting several practices recommended when performing this kind of operations.

      Note: Data-binding is the recommended best practice and this way of adding controls should be used sparingly and only when there is a real need to skip bindings.

      In order to illustrate how it is possible to add controls dynamically, you will create a control that loads a list of images at runtime. For the sake of simplicity the list of images will be hard-coded; however everything is created in a way that makes it easy to add a data source later. The recipe starts by creating a default Silverlight project.

      When a new Silverlight project is created, Visual Studio creates a default user control named 'Page'. The default XAML and code behind added by the Visual Studio template for the project are listed in Example 1-1 and Example 1-2.

      <!--Example 1-1. Default XAML generated by Visual Studio for the 'Page' user control:-->
      <UserControl x:Class="ASD.Cookbook.Page"
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
          Width="400" Height="300">
          <Grid x:Name="LayoutRoot" Background="White">
      
          </Grid>
      </UserControl>

       

      //Example 1-2. Default C# code generated by Visual Studio for the ‘Page’ user control:
      using System;
      using System.Collections.Generic;
      using System.Linq;
      using System.Net;
      using System.Windows;
      using System.Windows.Controls;
      using System.Windows.Documents;
      using System.Windows.Input;
      using System.Windows.Media;
      using System.Windows.Media.Animation;
      using System.Windows.Shapes;
      
      namespace ASD.Cookbook.Recipe0106 
      {
          public partial class Page : UserControl 
          {
              public Page() 
              {
                InitializeComponent();
              }
          }
      }

      Following, place a StackPanel inside the Grid control generated by Visual Studio, as shown in Example 1-3. A StackPanel is used here because it handles the arrangement by itself, reducing the need for customizing the UI properties of the controls which are added dynamically.

      <!--Example 1-3. A StackPanel added inside LayoutRoot:-->
      <Grid x:Name="LayoutRoot" Background="White">
          <StackPanel x:Name="ImageContainer" 
                      VerticalAlignment="Stretch" 
                      HorizontalAlignment="Stretch" 
                      Orientation="Horizontal">
          </StackPanel>
      </Grid>
      

      The recently added StackPanel is named ImageContainer. You will use this name later in code. The vertical and horizontal alignments have been set to Stretch so that the StackPanel takes all the space available. The orientation of the StackPanel has been set to horizontal so that the images stack from left to right when they are added to the panel.

      Now in code create a method that returns an enumeration of images that are loaded later in the StackPanel; as shown in Example 1-4.

      //Example 1-4. Method that returns the image data source:
      private IEnumerable<Uri> GetImageList() 
      {
          for (Int32 i = 0; i <= 10; i++) 
          {
              String uriString = 
                String.Format(
                "~/images/Image{0:00}.jpg"
                , i);
              yield return new Uri(uriString, UriKind.Relative);
          }
      }

      Then, use the list of URIs returned by the data source to instantiate a series of Image controls and add them to the list of children of the StackPanel, as shown in Example 1-5.

      //Example 1-5. Creating instances of the Image control for each URI in the data source:
      private void LoadImageList()
      {
          foreach(Uri imageUri in GetImageList())
          {
              Image imageControl = new Image();
              imageControl.Stretch = Stretch.UniformToFill;
              imageControl.Width = 30;
              imageControl.Height = 32;
              imageControl.Source =
                new System.Windows.Media.Imaging.BitmapImage(imageUri);
              ImageContainer.Children.Add(imageControl);
          }
      }

      You need to set a fixed width and height to the image controls to make as many items visible in the space available.

      Note: for the sake of simplicity the visual parameters are set in code. A better practice would be to create a style for the images in XAML and assign it when the instances are created.

      Finally you need to invoke the LoadImageList method from the constructor of the Page class as shown in example Example 1-6.

      //Example 1-6. Calling the LoadImageList method:
      public Page() 
      {
          InitializeComponent();
          LoadImageList();
      }

      When the application runs, the LoadImageList method is invoked at startup and the images are added to the StackPanel. Figure 1-3 shows the application while running, showing the list of images.

      Figure 1-3. Images are loaded when the application runs.

      Figure1-3

      Although adding controls dynamically is something that you will find yourself doing at some point as a developer, there are several considerations you should have in mind when doing so:

      · Each control you load dynamically uses memory and the larger your control is the more you need to be careful about the number of controls you load. Think about implementing pagination if you notice the memory usage is unusually high.

      · Try to encapsulate as much as you can and keep different functionality separate from each other. Something important to note in this example is how, even when the data is generated automatically instead of loaded from a data source, the loading operation is implemented in a different method. Try to keep the code that instantiates and adds the controls separate from the rest of the application.

      · Controls are initialized with their default values. If you want to set specific properties on the newly created controls, you have to do it manually on each or assign a style and/or template. This can become tedious and error prone, so if you think that you may need to set many properties from code, it is better for you to create a custom control and set the properties in XAML and instantiate the custom control dynamically.

      · The separation between the UI and the logic is good, so the ability to create instances of controls and add them to the UI from code does not mean you should disregard XAML in your applications. You should try to keep this to a minimum, delegating styling, templates, positioning, sizes and arrangements to XAML whenever possible and using code only for simple instantiating and adding, if strictly necessary. Sometimes it is just not possible to use data-binding; however, think of these cases as the exceptions, not the rule.

      Currently rated 3.0 by 2 people

      • Currently 3/5 Stars.
      • 1
      • 2
      • 3
      • 4
      • 5

      What is the Radial Panel?

      In WPF and Silverlight, it is possible to create custom panels by inheriting directly from the base Panel class. Custom panels arrange their children by overriding the MeasureOverride and ArrangeOverride methods to create new ways to arrange items on screen. The radial panel is a custom panel whose purpose is to arrange items within a certain radius of the center of the panel, each of the items is rotated at a certain angle to resemble a circle. There are several incarnations of the radial panel already on the web, some of them dating back to 2005:

      Why Another Radial Panel?

      With all these options available, at the beginning we thought it was a matter of choosing one and building our project. However, ASD required a Silverlight panel that enabled an arrangement of items that could be animated around the center, and unfortunately, the solutions available for Silverlight did not seem to support animations. Hence, the project.

      Project Requirements

      • A Silverlight panel that arranges items around the center, rotating them at a certain angle and at a certain distance from the center.
      • Each item should be able to set its own angle and radius.
      • It is necessary to attach animations to each of the items independently.

      Implementation

      Start by declaring the panel class, inheriting from Panel:

      public class RadialPanel : Panel
      {
      }

      Then, it is necessary to declare a series of attached properties, that are used by the children of the panel to inform their angle and radius. This is the implementation of the Angle property:

      public static readonly DependencyProperty AngleProperty =
          DependencyProperty.RegisterAttached(
          "Angle",
          typeof(Double),
          typeof(RadialPanel),
          new PropertyMetadata(0.0, new PropertyChangedCallback(OnAnglePropertyChanged))
          );
      public static void SetAngle(UIElement element, Double value)
      {
          element.SetValue(AngleProperty, value);
      }
      public static Double GetAngle(UIElement element)
      {
          return (Double)element.GetValue(AngleProperty);
      }
      private static void OnAnglePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) {
      }

      Notice that even though we have declared a property changed handler, it does not have an implementation. It is in this handler that the animation capabilities of the property are implemented and therefore, this method is necessary; however the handler code will be added later in the project as it depends on operations that are not implemented yet.
      Next, the Radius property is implemented in a similar way:

      public static readonly DependencyProperty RadiusProperty = DependencyProperty.RegisterAttached(
        "Radius",
        typeof(Double),
        typeof(RadialPanel),
        new PropertyMetadata(0.0, new PropertyChangedCallback(OnRadiusPropertyChanged))
      );
      public static void SetRadius(UIElement element, Double value)
      {
          element.SetValue(RadiusProperty, value);
      }
      public static Double GetRadius(UIElement element)
      {
          return (Double)element.GetValue(RadiusProperty);
      }
      private static void OnRadiusPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) {
      }

      Just like with the Angle property, the callback handler for the Radius property is not implemented yet. Additionally, it is necessary to declare a private attached property called Center, that will be used internally by the panel:

      private static readonly DependencyProperty CenterProperty = DependencyProperty.RegisterAttached(
        "Center",
        typeof(Point),
        typeof(RadialPanel),
        new PropertyMetadata(new Point(), null)
      );
      private static void SetCenter(UIElement element, Point value)
      {
          element.SetValue(CenterProperty, value);
      }
      private static Point GetCenter(UIElement element)
      {
          return (Point)element.GetValue(CenterProperty);
      }

      The center property does not require a property changed callback handler.
      With the properties in place, it is time to create an auxiliary method whose purpose is to arrange an item that lives inside the panel. This method encapsulates the logic related with the arrangement of the radial panel and will be used by other members of the class:

      private static void ArrangeChild(UIElement child)
      {
          child.RenderTransform = new RotateTransform();
          RotateTransform rotateTransform =
              child.RenderTransform as RotateTransform;
          rotateTransform.Angle = GetAngle(child);
          rotateTransform.CenterX =
              child.DesiredSize.Width / 2;
          rotateTransform.CenterY = -GetRadius(child);
          child.Arrange(new Rect(
              new Point(GetCenter(child).X - child.DesiredSize.Width / 2,
                  GetCenter(child).Y + GetRadius(child)),
              new Size(child.DesiredSize.Width,
                       child.DesiredSize.Height)));
      }

      The ArrangeChild method performs the following operations:

      • Receives the child it affects as a parameter.
      • Assigns a new RotateTransform to the RenderTransform property of the child.
      • Uses the GetAngle method of the Angle attached property to assign a value to the RotateTransform.Angle property.
      • Calculates and assignes the center of the RotateTransform using the width of the child and the Radius attached property.
      • Arranges the child using the value of the private attached property Center, as well as the Radius.

      Now we can override the MeasureOverride and ArrangementOverride methods to implement the arrangement of the items:

      protected override Size MeasureOverride(Size availableSize)
      {
          double maxChildRadius = 0;
          foreach (UIElement child in Children)
          {
              child.Measure(new
                  Size(Double.PositiveInfinity,
                       Double.PositiveInfinity));
              maxChildRadius =
                  Math.Max(maxChildRadius, GetRadius(child));
          }
          double squareSize = 2 * (maxChildRadius);
          return new Size(squareSize, squareSize);
      }
      
      protected override Size ArrangeOverride(Size finalSize)
      {
          var centerX = finalSize.Width / 2;
          var centerY = finalSize.Height / 2;
          foreach (UIElement child in Children)
          {
              child.SetValue(CenterProperty, new Point(centerX, centerY));
              ArrangeChild(child);
          }
      
          return finalSize;
      }

      MeasureOverride finds the size of the children and calculates the area the panel occupies on screen. ArrangeOverride iterates through the children, sets the center value and arranges using the auxiliary method ArrangeChild created previously. Finally we need to add the implementation of the property changed callback methods for Angle and Radius, in order to enable the support for animation:

      private static void OnAnglePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) {
          var child = sender as UIElement;
          ArrangeChild(child);
      }
      private static void OnRadiusPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) {
          var child = sender as UIElement;
          ArrangeChild(child);
      }

      The callback methods make sure that the child element is re-arranged whenever the property changes. This is more efficient than invalidating the arrange of the panel.

      Using the radial panel

      With the implementation complete, it is time to create an instance of the panel and add some children within:

      <Grid x:Name="LayoutRoot" Background="White">
          <Ellipse Stroke="Black" Fill="Gold" 
                   Width="50" Height="50" 
                   VerticalAlignment="Center" HorizontalAlignment="Center"/>
          <this:RadialPanel x:Name="TranslationRadialPanel" 
                            VerticalAlignment="Stretch" 
                            HorizontalAlignment="Stretch">
              <Ellipse Width="20" Height="20" 
                       this:RadialPanel.Angle="150" 
                       this:RadialPanel.Radius="45" 
                       Stroke="Black" Fill="Navy" x:Name="Ellipse01"/>
              <Ellipse Width="20" Height="20" 
                       this:RadialPanel.Angle="120" 
                       this:RadialPanel.Radius="75" 
                       Stroke="Black" Fill="Navy" x:Name="Ellipse02"/>
              <Ellipse Width="20" Height="20" 
                       this:RadialPanel.Angle="130" 
                       this:RadialPanel.Radius="90" 
                       Stroke="Black" Fill="Navy" x:Name="Ellipse03"/>
              <Ellipse Width="20" Height="20" 
                       this:RadialPanel.Angle="-30" 
                       this:RadialPanel.Radius="75" 
                       Stroke="Black" Fill="Navy" x:Name="Ellipse04"/>
              <Ellipse Width="20" Height="20" 
                       this:RadialPanel.Angle="250" 
                       this:RadialPanel.Radius="55" 
                       Stroke="Black" Fill="Navy" x:Name="Ellipse05"/>
              <Ellipse Width="20" Height="20" 
                       this:RadialPanel.Angle="230" 
                       this:RadialPanel.Radius="40" 
                       Stroke="Black" Fill="Navy" x:Name="Ellipse06"/>
              <Ellipse Width="20" Height="20" 
                       this:RadialPanel.Angle="50" 
                       this:RadialPanel.Radius="65" 
                       Stroke="Black" Fill="Navy" x:Name="Ellipse07"/>
              <Ellipse Width="20" Height="20" 
                       this:RadialPanel.Angle="150" 
                       this:RadialPanel.Radius="180" 
                       Stroke="Black" Fill="Crimson" x:Name="Ellipse08"/>
              <Ellipse Width="20" Height="20" 
                       this:RadialPanel.Angle="120" 
                       this:RadialPanel.Radius="160" 
                       Stroke="Black" Fill="Crimson" x:Name="Ellipse09"/>
              <Ellipse Width="20" Height="20" 
                       this:RadialPanel.Angle="130" 
                       this:RadialPanel.Radius="190" 
                       Stroke="Black" Fill="Crimson" x:Name="Ellipse10"/>
              <Ellipse Width="20" Height="20" 
                       this:RadialPanel.Angle="-30" 
                       this:RadialPanel.Radius="185" 
                       Stroke="Black" Fill="Crimson" x:Name="Ellipse11"/>
              <Ellipse Width="20" Height="20" 
                       this:RadialPanel.Angle="250" 
                       this:RadialPanel.Radius="165" 
                       Stroke="Black" Fill="Crimson" x:Name="Ellipse12"/>
              <Ellipse Width="20" Height="20" 
                       this:RadialPanel.Angle="230" 
                       this:RadialPanel.Radius="135" 
                       Stroke="Black" Fill="Crimson" x:Name="Ellipse13"/>
              <Ellipse Width="20" Height="20" 
                       this:RadialPanel.Angle="50" 
                       this:RadialPanel.Radius="180" 
                       Stroke="Black" Fill="Crimson" x:Name="Ellipse14"/>
          </this:RadialPanel>
      </Grid>

      This is how the arrangement looks like at runtime:

      Now, in order to animate the attached properties, it is necessary to add a storyboard to the resources of the Page user control:

      <UserControl.Resources>
          <Storyboard x:Key="TranslationStoryboard"/>
      </UserControl.Resources>

      As the number of items in the arrangement is considerable, we decided to automate the process of creating the animations in the code behind:

      private DoubleAnimationUsingKeyFrames GetAnimation(UIElement child, String name)
      {
      
          var random = new Random(DateTime.Now.Millisecond);
          var animation = new DoubleAnimationUsingKeyFrames()
          {
              RepeatBehavior = RepeatBehavior.Forever,
          };
          Storyboard.SetTargetName(animation, name);
          Storyboard.SetTargetProperty(animation, new PropertyPath(RadialPanel.AngleProperty));
          var keyframe = new SplineDoubleKeyFrame()
          {
              KeyTime = new TimeSpan(random.Next(20000000, 120000000)),
              KeySpline = new KeySpline()
              {
                  ControlPoint1 = new Point(0, 0),
                  ControlPoint2 = new Point(1, 1)
              },
              Value = RadialPanel.GetAngle(child) + 360
          };
          animation.KeyFrames.Add(keyframe);
          return animation;
      }

      Finally in the constructor of the page, the application iterates through the children of the radial panel and creates an animation for each of them:

      public Page()
      {
          InitializeComponent();
          var translationStoryboard = (Resources["TranslationStoryboard"] as Storyboard);
          foreach (var child in TranslationRadialPanel.Children) {
              if (child is FrameworkElement)
              {
                  var element = child as Ellipse;
                  translationStoryboard.Children.Add(GetAnimation(element, element.Name));
              }
          }
          translationStoryboard.Begin();
      }

      The resulting application animates the Angle property making each of the items inside the radial panel rotate around the center of the panel infinitely.
      You can get the complete code and see the demonstration here:

      Currently rated 5.0 by 2 people

      • Currently 5/5 Stars.
      • 1
      • 2
      • 3
      • 4
      • 5
      murven , created on 17. December 2008, 09:01

      Advanced Development Software is a fictional company in the field of innovation whose only purpose is to create cutting-edge mini projects using Microsoft-related technologies. Our expertise spans many different areas, including, but not limited to:

      • .NET Framework
      • C#
      • WPF
      • WCF
      • Silverlight
      • ASP.NET
      • ASP.NET AJAX
      • Microsoft Surface
      • Mono
      • Visual Studio
      • SQL Server
      • SharePoint

      Our goal is experimenting using these technologies and drawing conclusions on the potential of the tools we have at hand. We may not always succeed, but we sure will always have fun in the process. :-)

      This page represents the record of our works and the legacy we will leave to humanity if we ever cease to exist (not that we exist now, anyway, so this may go for a long time...)

      Be the first to rate this post

      • Currently 0/5 Stars.
      • 1
      • 2
      • 3
      • 4
      • 5