views:

312

answers:

3

Hi, I am creating a custom WPF control that should have several content slots. I'd like the user to be able to use either string, or a FrameworkElement as a value of property, for example:

<!-- MyHeading is a string -->
<MyControl MyHeading="Hello World" />

<MyControl>
 <!-- MyHeading is a FrameworkElement -->
 <MyControl.MyHeading>
  <Expander Header="Hello">
   World
  </Expander>
 </MyControl.MyHeading>
</MyControl>

I know that WPF ContentControl does this (accepts both strings and other elements), and I know that it has something to do with TypeConverter attribute (partially explained here), but I tried to look at ContentControl, Label, TextBlock and other controls in Reflector, and didn't find any TypeConverter atrribute there, and googling didn't help.

I first tried to implemet it like this, but it obviously doesn't know about how to convert string to FrameworkElement, and throws exception during control's initialization:

public FrameworkElement Heading
{
 get { return (FrameworkElement)GetValue(HeadingProperty); }
 set { SetValue(HeadingProperty, value); }
}

// Using a DependencyProperty as the backing store for Heading.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty HeadingProperty =
 DependencyProperty.Register("Heading", typeof(object), typeof(DialogControl), new UIPropertyMetadata(new FrameworkElement()));

Then I tried to hack it like this:

public object Heading
{
 get { return (object)GetValue(HeadingProperty); }
 set
 {
  if (value is string)
  {
   var tb = new TextBlock();
   tb.Text = (string) value;
   tb.FontSize = 20;
   SetValue(HeadingProperty, tb);
  }
  else if (value is FrameworkElement)
  {
   SetValue(HeadingProperty, value);
  } else 
   throw new ArgumentOutOfRangeException("Heading can take only string or FrameworkElement.");
 }
}

// Using a DependencyProperty as the backing store for Heading.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty HeadingProperty =
 DependencyProperty.Register("Heading", typeof(object), typeof(DialogControl), new UIPropertyMetadata(null));

but it is pretty ugly and still doesn't instantiate :(.

Anyone knows how to do it? Thanks for your time!

+1  A: 

The DependencyProperty should be of type Object. The magic happens in the when you bind the property as the Content for a ContentPresenter. You should also look into the ContentSource property if you want to handle Templating and StringFormatting properly.

Bryan Anderson
Thanks for clarifying that the conversion happens in ContentPresenter, that was very valuable info for me. The link to ContentSource also helped to do it properly, thank you!
Tomáš Kafka
+1  A: 

As Bryan said just use object as your type. When WPF encounters a non-frameworkelement it will (assuming there is no DataTemplate to apply) call the object's ToString() method and use the text as the content. So not only can you use string, but also DateTime, Enum's, whatever.

Also, you should consider deriving from HeaderedContentControl if your control has both a header and main content. Then you don't need to implement either of those two content properties and you'll get all the bells and whistles for free such as data templating.

Josh Einstein
Thanks for clarification, and yes, in my case deriving from HeaderedControl was sufficient, but I wanted to know how to do it on my own (for example when adding third 'content slot' to a HeaderedControl).
Tomáš Kafka
A: 

As the others have said, set the type to be object. To do type checking, use the validation callback on the dependency property. Then do checks for valid types.

public object Header
    {
        get { return (object)GetValue(HeadingProperty); }
        set { SetValue(HeadingProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Value.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty HeadingProperty = DependencyProperty.Register(
        "Heading",
        typeof(object),
        typeof(DialogControl),
        new UIPropertyMetadata(null),
        new ValidateValueCallback(Heading_Validation)
        );

    private static bool Heading_Validation(object source)
    {
        return source is string||
            source is FrameworkElement ||
            source == null;
    }

This will check to see, before assignment, if the passed object is of type String, FrameworkElement or null.

Enjoy!

Alastair Pitts
But then you lose the ability to support data templates or non-string types that are still printable.
Josh Einstein
Indeed, i ripped this code from my project where there is specific objects that needed drawing. You are correct tho, for a header, this is probably not the best implementation.
Alastair Pitts
Not what I needed, but useful trick anyway, thanks.
Tomáš Kafka