none
styling generic WPF controls

    Question

  • I am looking into creating type-safe generic controls. This is targeting the (reduced) generics support in WPF 4 and future Silverlight, and will include a hierarchy of generic controls. (Since this question has parts for both WPF and Silverlight, I have posted the same question to the Silverlight forums .)

    I have two questions:

    1. Can you use style setters and template bindings for non-generic properties defined on a generic control?
    2. In Silverlight, is there a value I can use for the default style key in the base class that will allow using the same style in the (temporary) specific-type derived classes? (ComponentResourceKey does not exist in Silverlight, so the setup described below does not work.)

     


    The test generic control below defines two test properties: a non-generic Description property, and a generic Data property. The control sets DefaultStyleKey to a ComponentResourceKey for the control.

    Here is how the test control is defined:

    public class GenericControl<T> : Control {
      static GenericControl( ) {
        DefaultStyleKeyProperty.OverrideMetadata(
          typeof(GenericControl<T>), new FrameworkPropertyMetadata(
            new ComponentResourceKey( typeof(Proxy), "GenericControl`1" )
          )
        );
      }
    
      public static readonly DependencyProperty DescriptionProperty =
        DependencyProperty.Register(
          "Description", typeof(string), typeof(GenericControl<T>),
          new PropertyMetadata( "Default Description" )
        );
      public static readonly DependencyProperty DataProperty =
        DependencyProperty.Register(
          "Data", typeof(T), typeof(GenericControl<T>),
          new PropertyMetadata( default(T) )
        );
    
      public string Description { get { ... } set { ... } }
      public T Data { get { ... } set { ... } }
    }
    

    Here is the style for the test control in generic.xaml :

    <Style x:Key="{ComponentResourceKey {x:Type local:Proxy}, GenericControl`1}">
      <Setter Property="Control.Template">
        <Setter.Value>
          <ControlTemplate TargetType="{x:Type Control}">
            <Border Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}"">
              <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Description,
                                  RelativeSource={RelativeSource TemplatedParent}}" />
                <TextBlock Text="{Binding Data,
                                  RelativeSource={RelativeSource TemplatedParent}}" />
              </StackPanel>
            </Border>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
    

    Here are some examples of how this test control would be declared in xaml:

    <ListBox Name="list" ... />
    <GenericControl x:TypeArguments="sys:Int32" Description="Count: "
                    Data="{Binding Items.Count, ElementName=list}" />
    
    <Slider Name="slider" ... />
    <GenericControl x:TypeArguments="sys:Double" Description="Slider Value: "
                    Data="{Binding Value, ElementName=slider}" />
    

    With the current generics support in WPF 4, you cannot use an open generic type as the TargetType of a style or control template (doing so results in a "'GenericControl`1' TargetType does not match type of element 'GenericControl`1'." exception). This has two main consequences, as mentioned in question 1 above:

    • You must use a normal binding with RelativeSource={RelativeSource TemplatedParent} instead of a TemplateBinding in the control template to reference properties defined by the generic control.
    • You cannot create a style setter for the Description property, even though it does not depend on the generic type of the control.

     

    For the latter, there is a workaround in WPF: just define the non-generic properties as attached dependency properties on a proxy type. Then you can use AddOwner to "declare" the properties on the generic control, and you can use "ProxyType.Property" syntax in a style setter. Of course, Silverlight does not support AddOwner , and turning what is supposed to be an instance property into an attached property is not ideal in any case, so this is not really a long-term solution.

    • Edited by Emperor XLII Tuesday, January 26, 2010 2:46 PM re-inserted code blocks to fix coloring
    Tuesday, January 19, 2010 4:04 PM

Answers

  • Please note that the new generics support in XAML is available only in loose XAML (XamlReader.Load), not in compiled BAML (Build Action = Page). I wrote the following sample as a proof of concept, which works.

    <Application x:Class="GenericControlSample.App"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 Startup="Application_Startup">
        <Application.Resources>
            
        </Application.Resources>
    </Application>

        public partial class App : Application
        {
            private void Application_Startup(object sender, StartupEventArgs e)
            {
                Window window = (Window)XamlReader.Load(XmlReader.Create("MainWindow.xaml"));
                window.ShowDialog();
            }
        }

        public class GenericControl<T> : Control
        {
            static GenericControl()
            {
                //DefaultStyleKeyProperty.OverrideMetadata(typeof(GenericControl<T>), new FrameworkPropertyMetadata(typeof(GenericControl<String>)));
            }

            public GenericControl()
            {
                this.Resources=(ResourceDictionary)XamlReader.Load(XmlReader.Create(@"Themes\Generic.xaml"));
            }

            public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register("Description", typeof(string), typeof(GenericControl<T>), new FrameworkPropertyMetadata("Default Description"));
            public static readonly DependencyProperty DataProperty = DependencyProperty.Register("Data", typeof(T), typeof(GenericControl<T>), new PropertyMetadata(default(T)));

            public string Description
            {
                get { return (string)this.GetValue(DescriptionProperty); }
                set { this.SetValue(DescriptionProperty, value); }
            }

            public T Data
            {
                get { return (T)this.GetValue(DataProperty); }
                set { this.SetValue(DataProperty, value); }
            }
        }

    Generic.xaml (Build Action = Content, Copy To Output Directory = Always)
    <ResourceDictionary
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:GenericControlSample;assembly=GenericControlSample">
       
        <Style TargetType="{x:Type local:GenericControl(x:Int32)}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type local:GenericControl(x:Int32)}">
                        <Border Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}">
                            <StackPanel>
                                <TextBlock>GenericControl&lt;Int32&gt;</TextBlock>
                                <TextBlock Text="{Binding Description, RelativeSource={RelativeSource TemplatedParent}}" />
                                <TextBlock Text="{Binding Data, RelativeSource={RelativeSource TemplatedParent}}" />
                            </StackPanel>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style TargetType="{x:Type local:GenericControl(x:String)}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type local:GenericControl(x:String)}">
                        <Border Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}">
                            <StackPanel>
                                <TextBlock>GenericControl&lt;String&gt;</TextBlock>
                                <TextBlock Text="{Binding Description, RelativeSource={RelativeSource TemplatedParent}}" />
                                <TextBlock Text="{Binding Data, RelativeSource={RelativeSource TemplatedParent}}" />
                            </StackPanel>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ResourceDictionary>

    MainWindow.xaml (Build Action = Content, Copy To Output Directory = Always)
    <Window
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:GenericControlSample;assembly=GenericControlSample"
            Title="MainWindow" Height="350" Width="525">
        <StackPanel>
            <local:GenericControl x:TypeArguments="x:Int32" Data="2" Description="SampleDesc"></local:GenericControl>
            <local:GenericControl x:TypeArguments="x:String" Data="2" Description="SampleDesc"></local:GenericControl>
        </StackPanel>
    </Window>

    • Marked as answer by Jim Zhou - MSFT Wednesday, January 27, 2010 9:06 AM
    • Unmarked as answer by Emperor XLII Wednesday, January 27, 2010 8:01 PM
    • Marked as answer by Emperor XLII Thursday, January 28, 2010 2:44 PM
    Tuesday, January 26, 2010 9:00 PM
  • Thank you! That does get closer to a solution than I was able to reach (I had not thought of setting TargetType to a concrete type).

    However, the solution as presented is not scalable. It will only work if you limit the control to supporting a fixed set of pre-defined types (and even then, you must manually copy-and-paste the exact same template over and over again for every type in that set). It does not allow you to create a single template that can be used by all instances of the control.

    I was about to write how, if control templates and resource dictionaries just supported TargetType="{x:Type local:GenericControl`1}" , all these issues would go away. Thinking about it, though, I can see the issue with trying to construct a template when you can only test that the dependency properties will be available once you have a closed type (rather than working with the actual dependency property instances as you would with a normal type), or trying to determine what the best-fit key is in a set of open generic types. I certainly don't meant to discourage you from adding such support, but I am no longer as optimistic :)

    Instead, here is a quick-and-dirty attempt at hacking in support for arbitrary types, rather than a fixed set. This is a replacement for the static type constructor from Shreedhar's answer:

    static GenericControl( ) {
      string template = @"
    <ControlTemplate TargetType='{x:Type local:GenericControl(type:TYPE)}'
      xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
      xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
      xmlns:type='clr-namespace:NAMESPACE;assembly=ASSEMBLY'
      xmlns:local='clr-namespace:GenericControlSample;assembly=GenericControlSample'>
      <Border Background='{TemplateBinding Background}'
        BorderBrush='{TemplateBinding BorderBrush}'
        BorderThickness='{TemplateBinding BorderThickness}'>
        <StackPanel>
          <TextBlock>GenericControl&lt;TYPE&gt;</TextBlock>
          <TextBlock Text='{TemplateBinding Description}' />
          <TextBlock Text='{Binding Data, RelativeSource={RelativeSource TemplatedParent}}' />
        /StackPanel>
      </Border>
    </ControlTemplate>
    ";
      string xaml = template
        .Replace( "TYPE", typeof( T ).Name )
        .Replace( "NAMESPACE", typeof( T ).Namespace )
        .Replace( "ASSEMBLY", new AssemblyName( typeof( T ).Assembly.FullName ).Name )
        ;
      using( Stream s = new MemoryStream( ASCIIEncoding.Default.GetBytes( xaml ) ) ) {
        object o = XamlReader.Load( s );
        TemplateProperty.OverrideMetadata( typeof( GenericControl<T> ), new FrameworkPropertyMetadata( o ) );
      }
    }
    

     

    This overrides the default value for the Template property of the GenericControl control, constructing a type-specific version of the base template for the T type argument. I can already see a few flaws with this approach (e.g. the control can support exactly one general-purpose template, theme-based styles or alternate templates would have to use the copy-and-paste method to define the same template for all of the types the control is used with, OverrideMetadata does not exist in the current version of Silverlight 3, etc).

     

    Do you have any better ideas for overcoming this last hurdle to creating a truly first-class generic control?

    • Marked as answer by Emperor XLII Thursday, January 28, 2010 2:44 PM
    Wednesday, January 27, 2010 8:01 PM
  • Creating templates on the fly is a nice touch! I dont have any other suggestions for you. XAML doesn't currently support specifying open generic types.

    • Marked as answer by Emperor XLII Thursday, January 28, 2010 2:44 PM
    Thursday, January 28, 2010 3:25 AM

All replies

  • Hi,

    -->1.Can you use style setters and template bindings for non-generic properties defined on a generic control?

    As far as I know , we can use Style to define the non-generic property and reuse the style in our application.
    -->2.In Silverlight, is there a value I can use for the default style key in the base class that will allow using the same style in the (temporary) specific-type derived classes? (ComponentResourceKey does not exist in Silverlight, so the setup described below does not work.)

    Since this is WPF forum, and this is a specific Silverlight related question, so I recommend that you can post this sub-question to Silverlight forum to confirm.


    If you are still having any issues this this, please feel free to feed back.
    Thanks.

    Jim Zhou -MSFT
    Thursday, January 21, 2010 6:22 AM
  • Hi Emperor XLII,

    Could you please let me know the status of this thread? I am looking forward to hearing from you.

    Thanks.
    Sincerely.
    Jim Zhou -MSFT
    Monday, January 25, 2010 1:15 PM
  • Sorry for not responding earlier (the forum did not send me an alert for your first reply).

    -->1. Can you use style setters and template bindings for non-generic properties defined on a generic control?

    As far as I know, we can use Style to define the non-generic property and reuse the style in our application.

    I agree that you should be able to do it, but I could not get it to work. A simple <Setter Property="Description" Value="Default" /> setter in the style does not work (this is the second bullet in the problem description, after the code examples).

    -->2. In Silverlight, is there a value I can use for the default style key in the base class that will allow using the same style in the (temporary) specific-type derived classes? (ComponentResourceKey does not exist in Silverlight, so the setup described below does not work.)

    Since this is WPF forum, and this is a specific Silverlight related question, so I recommend that you can post this sub-question to Silverlight forum to confirm.

    Yes, I did cross-post this question on the Silverlight forums . I wanted to make sure each forum post had all the information, so I used the same wording for both.

    Tuesday, January 26, 2010 2:50 PM
  • Please note that the new generics support in XAML is available only in loose XAML (XamlReader.Load), not in compiled BAML (Build Action = Page). I wrote the following sample as a proof of concept, which works.

    <Application x:Class="GenericControlSample.App"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 Startup="Application_Startup">
        <Application.Resources>
            
        </Application.Resources>
    </Application>

        public partial class App : Application
        {
            private void Application_Startup(object sender, StartupEventArgs e)
            {
                Window window = (Window)XamlReader.Load(XmlReader.Create("MainWindow.xaml"));
                window.ShowDialog();
            }
        }

        public class GenericControl<T> : Control
        {
            static GenericControl()
            {
                //DefaultStyleKeyProperty.OverrideMetadata(typeof(GenericControl<T>), new FrameworkPropertyMetadata(typeof(GenericControl<String>)));
            }

            public GenericControl()
            {
                this.Resources=(ResourceDictionary)XamlReader.Load(XmlReader.Create(@"Themes\Generic.xaml"));
            }

            public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register("Description", typeof(string), typeof(GenericControl<T>), new FrameworkPropertyMetadata("Default Description"));
            public static readonly DependencyProperty DataProperty = DependencyProperty.Register("Data", typeof(T), typeof(GenericControl<T>), new PropertyMetadata(default(T)));

            public string Description
            {
                get { return (string)this.GetValue(DescriptionProperty); }
                set { this.SetValue(DescriptionProperty, value); }
            }

            public T Data
            {
                get { return (T)this.GetValue(DataProperty); }
                set { this.SetValue(DataProperty, value); }
            }
        }

    Generic.xaml (Build Action = Content, Copy To Output Directory = Always)
    <ResourceDictionary
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:GenericControlSample;assembly=GenericControlSample">
       
        <Style TargetType="{x:Type local:GenericControl(x:Int32)}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type local:GenericControl(x:Int32)}">
                        <Border Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}">
                            <StackPanel>
                                <TextBlock>GenericControl&lt;Int32&gt;</TextBlock>
                                <TextBlock Text="{Binding Description, RelativeSource={RelativeSource TemplatedParent}}" />
                                <TextBlock Text="{Binding Data, RelativeSource={RelativeSource TemplatedParent}}" />
                            </StackPanel>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style TargetType="{x:Type local:GenericControl(x:String)}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type local:GenericControl(x:String)}">
                        <Border Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}">
                            <StackPanel>
                                <TextBlock>GenericControl&lt;String&gt;</TextBlock>
                                <TextBlock Text="{Binding Description, RelativeSource={RelativeSource TemplatedParent}}" />
                                <TextBlock Text="{Binding Data, RelativeSource={RelativeSource TemplatedParent}}" />
                            </StackPanel>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ResourceDictionary>

    MainWindow.xaml (Build Action = Content, Copy To Output Directory = Always)
    <Window
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:GenericControlSample;assembly=GenericControlSample"
            Title="MainWindow" Height="350" Width="525">
        <StackPanel>
            <local:GenericControl x:TypeArguments="x:Int32" Data="2" Description="SampleDesc"></local:GenericControl>
            <local:GenericControl x:TypeArguments="x:String" Data="2" Description="SampleDesc"></local:GenericControl>
        </StackPanel>
    </Window>

    • Marked as answer by Jim Zhou - MSFT Wednesday, January 27, 2010 9:06 AM
    • Unmarked as answer by Emperor XLII Wednesday, January 27, 2010 8:01 PM
    • Marked as answer by Emperor XLII Thursday, January 28, 2010 2:44 PM
    Tuesday, January 26, 2010 9:00 PM
  • Thank you! That does get closer to a solution than I was able to reach (I had not thought of setting TargetType to a concrete type).

    However, the solution as presented is not scalable. It will only work if you limit the control to supporting a fixed set of pre-defined types (and even then, you must manually copy-and-paste the exact same template over and over again for every type in that set). It does not allow you to create a single template that can be used by all instances of the control.

    I was about to write how, if control templates and resource dictionaries just supported TargetType="{x:Type local:GenericControl`1}" , all these issues would go away. Thinking about it, though, I can see the issue with trying to construct a template when you can only test that the dependency properties will be available once you have a closed type (rather than working with the actual dependency property instances as you would with a normal type), or trying to determine what the best-fit key is in a set of open generic types. I certainly don't meant to discourage you from adding such support, but I am no longer as optimistic :)

    Instead, here is a quick-and-dirty attempt at hacking in support for arbitrary types, rather than a fixed set. This is a replacement for the static type constructor from Shreedhar's answer:

    static GenericControl( ) {
      string template = @"
    <ControlTemplate TargetType='{x:Type local:GenericControl(type:TYPE)}'
      xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
      xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
      xmlns:type='clr-namespace:NAMESPACE;assembly=ASSEMBLY'
      xmlns:local='clr-namespace:GenericControlSample;assembly=GenericControlSample'>
      <Border Background='{TemplateBinding Background}'
        BorderBrush='{TemplateBinding BorderBrush}'
        BorderThickness='{TemplateBinding BorderThickness}'>
        <StackPanel>
          <TextBlock>GenericControl&lt;TYPE&gt;</TextBlock>
          <TextBlock Text='{TemplateBinding Description}' />
          <TextBlock Text='{Binding Data, RelativeSource={RelativeSource TemplatedParent}}' />
        /StackPanel>
      </Border>
    </ControlTemplate>
    ";
      string xaml = template
        .Replace( "TYPE", typeof( T ).Name )
        .Replace( "NAMESPACE", typeof( T ).Namespace )
        .Replace( "ASSEMBLY", new AssemblyName( typeof( T ).Assembly.FullName ).Name )
        ;
      using( Stream s = new MemoryStream( ASCIIEncoding.Default.GetBytes( xaml ) ) ) {
        object o = XamlReader.Load( s );
        TemplateProperty.OverrideMetadata( typeof( GenericControl<T> ), new FrameworkPropertyMetadata( o ) );
      }
    }
    

     

    This overrides the default value for the Template property of the GenericControl control, constructing a type-specific version of the base template for the T type argument. I can already see a few flaws with this approach (e.g. the control can support exactly one general-purpose template, theme-based styles or alternate templates would have to use the copy-and-paste method to define the same template for all of the types the control is used with, OverrideMetadata does not exist in the current version of Silverlight 3, etc).

     

    Do you have any better ideas for overcoming this last hurdle to creating a truly first-class generic control?

    • Marked as answer by Emperor XLII Thursday, January 28, 2010 2:44 PM
    Wednesday, January 27, 2010 8:01 PM
  • Creating templates on the fly is a nice touch! I dont have any other suggestions for you. XAML doesn't currently support specifying open generic types.

    • Marked as answer by Emperor XLII Thursday, January 28, 2010 2:44 PM
    Thursday, January 28, 2010 3:25 AM
  • Emperor XLII, have you done any more with this? I'm struggling with this issue as well. Your statement:

    However, the solution as presented is not scalable. It will only work if you limit the control to supporting a fixed set of pre-defined types (and even then, you must manually copy-and-paste the exact same template over and over again for every type in that set). It does not allow you to create a single template that can be used by all instances of the control.

    is exactly what I have right now... ugly.

     


    Janene
    • Edited by JaneneMc Thursday, October 14, 2010 6:27 PM format, blech
    Thursday, October 14, 2010 6:25 PM