views:

239

answers:

4

I'm writing a WPF User Control for my application, wrapping a ListBox and a few other items.

The ListBox has a new ItemTemplate that presents four pieces of information for each item in my list. I can hard code each of the four bindings to specific properties on my list items and they display fine.

However, I want my UserControl to be a bit more flexible.

On ListBox and ComboBox there is a property DisplayMemberPath (inherited from ItemsControl) that seems to "inject" the appropriate property binding into the standard ItemTemplate.

How do I achieve the same result with my user control?

I'd like to set up four new properties to allow configuration of the information displayed:

public string LabelDisplayPath { get; set; } 
public string MetricDisplayPath { get; set; }
public string TitleDisplayPath { get; set; }
public string SubtitleDisplayPath { get; set; }

Reviewing ItemsControl.DisplayMemberPath with Reflector seems to go down the rabbit hole, I haven't been able to fathom how it works.

Also, if I'm completely off course - and there's another, more "WPF" technique that I should be using instead, please point me in that direction.

Update

Here's a clarification of what I'm trying to achieve.

The ListBox within my user control displays four pieces of information per item: Label, Title, Subtitle and Metric

In one place, I want to use this User control to display a list of issues. Each issue looks like this:

public class Issue {
    public string Code { get; set; }
    public string Description { get; set; }
    public string Priority { get; set; }
    public string Reporter { get; set; }
}

When displaying issues, I want to use the following mappings:

Code --> Label
Description --> Title
Reporter --> Subtitle
Priority --> Metric

Elsewhere in the same application, I have a list of Posts that I want to display using the same UserControl. Each Post looks like this:

public class Post {
    public DateTime PostedOn { get; set; }
    public string Title { get; set; }
    public string Teaser { get; set; }
    public int CommentCount { get; set; }
}

When displaying posts, I want to use the following mappings:

PostedOn --> Label
Title --> Title
Teaser --> Subtitle
CommentCount --> Metric

Given that Issues and Posts are quite different abstractions, I don't want to force them to have the same properties, just to allow the UserControl to be used unchanged. Instead, I want to introduce a little configurability so that I can reuse my UserControl in both sites cleanly.

A: 

After a lot of hints from your update and comment, i think you can achieve that using dependencyProperty

Let me give you the outline of the idea

XAML:

<UserControl x:Name="myControl">
    <StackPanel>
        <TextBlock x:Name="textBlock" 
                   Text="{Binding ElementName=myControl, Path=LabelDisplayPath}" />
        <TextBlock x:Name="textBlock" 
                   Text="{Binding ElementName=myControl, Path=MetricDisplayPath}" />
        <TextBlock x:Name="textBlock" 
                   Text="{Binding ElementName=myControl, Path=TitleDisplayPath}" />
        <TextBlock x:Name="textBlock" 
                   Text="{Binding ElementName=myControl, Path=SubtitleDisplayPath}" />
    </StackPanel>
</UserControl>

where DisplayPaths are the dependency properties

Now Create these Dependency Properties in the UserControl code behind: LabelDisplayPathProperty, MetricDisplayPathProperty...

Now you can use them like the inbuilt DisplayMemberPath property like this:

<ListView ItemsSource="{Binding IssueList}"> 
    <ListView.ItemTemplate>
        <DataTemplate>
            <myUC:Control1 LabelDisplayPath = "{Binding Path=Issue.Code}"
                           MetricDisplayPath = "{Binding Path=Issue.Priority}"
                           TitleDisplayPath = "{Binding Path=Issue.Description}"
                           SubtitleDisplayPath = "{Binding Path=Issue.Reporter}" />
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>
Veer
A: 

I think your best bet is to use data templates.
If you want to make it flexible, then define a default data template in your resource library (you can do this by simply not defining an x:Key or x:Name for the item). Create one data template for each different class you wish to display. Then simply bind/fill your list with classes and they will be displayed using the default data template :)

TerrorAustralis
This seems to violate the DRY principle (Don't Repeat Yourself). I don't want to have a different look and feel for each situation, just different information shown within the existing template. Any changes to the look and feel of the user control would have to be repeated identically on each (every) template.
Bevan
+1  A: 

I would use ViewModels as wrappers in this case to unify the two cases: You could create an (abstract) ItemViewModelBase class which defines the properties Label, Title, Subtitle, and Metric. Then you create concrete classes deriving from this base VM for all the items you want to display using the same control. Each of these classes returns something different in the properties.
This way, you can define one DataTemplate for the ItemViewModelBase class, and it will be applied to both item types.

Some code may make this clearer:

public abstract class ItemViewModelBase
{
    public abstract string Label { get; }
    public abstract string Title { get; }
    public abstract string Subtitle { get; }
    public abstract string Metric { get; }
}

public class IssueViewModel : ItemViewModelBase
{
    private Issue _issue;

    public override string Label { get { return _issue.Code; } }
    public override string Title { get { return _issue.Description; } }
    public override string Subtitle { get { return _issue.Reporter; } }
    public override string Metric { get { return _issue.Priority; } }

    public IssueViewModel(Issue issue)
    {
        _issue = issue;
    }
}

public class PostViewModel : ItemViewModelBase
{
    private Post _post;

    public override string Label { get { return _post.PostedOn.ToString(); } }
    public override string Title { get { return _post.Title; } }
    public override string Subtitle { get { return _post.Teaser; } }
    public override string Metric { get { return _post.CommentCount.ToString(); } }

    public PostViewModel(Issue post)
    {
        _post= post;
    }
}

In your ListBox you will then display a collection of ItemViewModelBase instances, rather than Issue and Post instances, which will be rendered using one common DataTemplate, like this:

<DataTemplate DataType="{x:Type local:ItemViewModelBase}">
    <StackPanel>
        <TextBlock x:Name="txtLabel" Text="{Binding Label}"/>
        <TextBlock x:Name="txtTitle" Text="{Binding Title}"/>
        <TextBlock x:Name="txtSubtitle" Text="{Binding Subtitle}"/>
        <TextBlock x:Name="txtMetric" Text="{Binding Metric}"/>
    </StackPanel>
</DataTemplate>

Of course, your DataTemplate will look differently, the above is just an example to demonstrate the principle. Furthermore, you might define other return types for the properties - I made them all strings, although you have a DateTime and an int in your data classes. One more thing I would not do in production code: DateTime.ToString(), as I did in PostViewModel.Label - better replace it with some localized string representation.

gehho
I could use a variation of this - retaining the individual `Issue`, `Post` and other classes, wrapping each in a custom wrapper as an adapter. Worth following up - thanks.
Bevan
Yes, that's exactly what I proposed. Just that you call it wrapper/adapter and I call it ViewModel. ;)
gehho
A: 

I've found the solution to my problem - it's a bit more complicated than I'd like ...

On any ItemsControl, when you set the DisplayMemberPath property, an instance of the internal class DisplayMemberTemplateSelector is assigned to the ItemTemplateSelector. In effect, this class generates a custom DataTemplate with an appropriate binding to suit the current configuration of DisplayMemberPath. A new instance of this class is created and used every time that DisplayMemberPath is changed.

To replicate this on my own Usercontrol, instead of modifying my existing data template on the fly as I had originally intended, I need to create my own template selector helper class and mimic the same approach as ItemsControl.

Bevan