I have faced a problem recently that required all sorts of different data to be displayed in a single list. Now this probably is not the first time this kind of requirement has surfaced. The requirement was to list entities that could have documents linked to them. These entities don’t really have anything in common, and in fact we don’t even know about these entities as the Document code is independent of anything that consumes it.
Ignoring the requirements for a moment lets just show the problem space. The problem is that we need to display a list of entities of differing types with enough information to identify the entity. Identifying entities is more difficult than just a type + Id/Key pair. Many of the entities are transient or the user may need more information to accurately identify the entity. So we have decided that we need a column for Type, ID/Key, Name and Description/Detail. Further to this the UI designer also wanted space to display comprehensive information about the entity.
So from the information I have from the requirements and the UI guy I decide that the best way to do this is an interface that entities can implement and then they can be displayed on the list.
public interface ILinkedEntity { string Type { get; } string Key { get; } string Name { get; } string Detail { get; } string Preview { get; } }
Then I could display the list like the following
Then I thought to myself that exposing Detail and Preview as strings is a stupid Idea because then I would be require presentation logic via string.format in the entity. So I changed the interface to look like this:
public interface ILinkedEntity { string Type { get; } string Key { get; } string Name { get; } object Detail { get; } object Preview { get; } }
Now it is up to the implementer of the interface to return an object that has a DataTemplate ready to be applied to it.
An example of an implementation:
public class Deal : ILinkedEntity { public int DealId { get; set; } public string PortfolioNumber { get; set; } public string PortfolioGroup { get; set; } public int RiskGrade { get; set; } public DateTime? DueDate { get; set; } #region ILinkedEntity Members public string Type { get { return "Deal"; } } public string Key { get { return DealId.ToString(); } } public string Name { get { return DealId.ToString(); } } public object Detail { get { return DealDescriptionTranslator.Translate(this); } } public object Preview { get { return this; } } #endregion }
So I create the XAML for the GridView and set the bindings to the correct paths on my ObservableCollection<ILinkedEntity> member.
<ListView x:Name="LinkedEntityList" ItemsSource="{Binding Path=LinkedEntities}"> <ListView.Resources> <Style TargetType="ListViewItem"> <Setter Property="ToolTip" Value="{Binding Path=Preview}"/> </Style> </ListView.Resources> <ListView.View> <GridView> <GridViewColumn Header="Type" DisplayMemberBinding="{Binding Path=Type}"/> <GridViewColumn Header="Id/Key" DisplayMemberBinding="{Binding Path=Key}"/> <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Path=Name}"/> <GridViewColumn Header="Detail" > <GridViewColumn.CellTemplate> <DataTemplate> <ContentControl Content="{Binding Path=Detail}" ToolTip="{Binding Path=Preview}" HorizontalContentAlignment="Stretch" HorizontalAlignment="Stretch" /> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> </GridView> </ListView.View> </ListView>
This all works well except I haven’t provided any DataTemplates for the Objects I return for Detail or Preview members. So I do this as per the UI guy’s specs and put them in a Resource Dictionary that shares scope with the view (app.xaml because Im lazy).
<DataTemplate DataType="{x:Type entities:Deal}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Label Grid.Column="0" Grid.Row="0" VerticalAlignment="Center">Deal Id :</Label> <TextBlock Grid.Column="1" Grid.Row="0" VerticalAlignment="Center" Text="{Binding Path=DealId}" /> <Label Grid.Column="0" Grid.Row="1" VerticalAlignment="Center">Portfolio :</Label> <TextBlock Grid.Column="1" Grid.Row="1" VerticalAlignment="Center" Text="{Binding Path=PortfolioNumber}" /> <Label Grid.Column="0" Grid.Row="2" VerticalAlignment="Center">Portfolio Group :</Label> <TextBlock Grid.Column="1" Grid.Row="2" VerticalAlignment="Center" Text="{Binding Path=PortfolioGroup}" /> <Label Grid.Column="2" Grid.Row="0" VerticalAlignment="Center">Risk Grade :</Label> <TextBlock Grid.Column="3" Grid.Row="0" VerticalAlignment="Center" Text="{Binding Path=RiskGrade}" /> <Label Grid.Column="2" Grid.Row="1" VerticalAlignment="Center">Due Date:</Label> <TextBlock Grid.Column="3" Grid.Row="1" VerticalAlignment="Center" Text="{Binding Path=DueDate}" /> </Grid> </DataTemplate> <DataTemplate DataType="{x:Type entities:DealDescription}"> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Path=PortfolioGroup}"/> <TextBlock Text="["/> <TextBlock Text="{Binding Path=PortfolioNumber}"/> <TextBlock Text="]"/> </StackPanel> </DataTemplate>
Right now I’m happy. I am now presenting data as per the requirements and UI design. The only problem is that I am Implementing the Interface implicitly. This creates a lot of noise on some of my entities that have members that are very similar to the ones defined on the interface. For example I have an “DealId” property that is an integer on the Deal object. For a consumer of the object it would be very confusing for them to see both a DealId and a Key property. So I decide to implement the interface explicitly to clean things up.
SHOCK!
My view is now broken! The view presents nothing but blank rows to the user. After a bit of time on the Google-machine I find post on a forum where Josh Smith offers some help. A quick change to the bindings on my XAML to use the interface explicitly :
<ListView x:Name="LinkedEntityList" ItemsSource="{Binding Path=LinkedEntities}"> <ListView.Resources> <Style TargetType="ListViewItem"> <Setter Property="ToolTip" Value="{Binding Path=Preview}"/> </Style> </ListView.Resources> <ListView.View> <!-- this version allows for Explicit interface implementation--> <GridView> <GridViewColumn Header="Type" DisplayMemberBinding="{Binding Path=(local:ILinkedEntity.Type)}"/> <GridViewColumn Header="Id/Key" DisplayMemberBinding="{Binding Path=(local:ILinkedEntity.Key)}"/> <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Path=(local:ILinkedEntity.Name)}"/> <GridViewColumn Header="Detail" > <GridViewColumn.CellTemplate> <DataTemplate> <ContentControl Content="{Binding Path=(local:ILinkedEntity.Detail)}" ToolTip="{Binding Path=(local:ILinkedEntity.Preview)}" HorizontalContentAlignment="Stretch" HorizontalAlignment="Stretch" /> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> </GridView> </ListView.View> </ListView>
Ta-dah! Thanks Josh. All happy now. I have a list that can display things it knows nothing about and hand off any fancy logic to a DataTemplate that should be defined by the UI guy in change of that domain.
Sweet.
Check out full code here
4 comments:
Hi Lee,
James here.
While the binding works fine, sorting has become a bit of an issue.
Trying to fix. See;
http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/9c49911c-4bcd-4d95-b467-64d39804577a
Cheers,
James
Hi,
Anybody know if it’s possible to bind to an indexer declared in an explicit interface implementation?
Would like to do somthing like this: Text="{Binding Path=(local:IMyInterface[identifyer])}"
Text="{Binding Path=(local:IMyInterface.Texts[identifyer])}" where Texts returns an class that has an indexer
Best regards
Sanny
I am not sure off the top of my head if binding supports what you want. I am sure you could do what you want if you used a Multivalue converter.
It could cast the first value to an IList and the second value to an int.
If you were happy to hard code the index then
works fine.
Given a MultiConverter like this
public class ListIndexConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var list = (IList)values[0];
var index = (int) values[1];
return list[index];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
you can use a multibinding like this
<TextBox>
<TextBox.Text>
<MultiBinding Converter="{StaticResource listIndexConverter}" Mode="OneWay">
<Binding Path="(local:IMyInterface.Texts)" />
<Binding Path="(local:IMyInterface.SelectedIndex)" />
</MultiBinding>
</TextBox.Text>
</TextBox>
Post a Comment