DataTemplate for an array or IEnumerable - c#

I would like to create an implicit DataTemplate that works on an array or IEnumerable of my class. This way I have a template that describes how to render a bunch of items instead of just one. I want to do this so I can, among other things, show the results in a tooltip. eg
<TextBlock Text="{Binding Path=CustomerName}" ToolTip="{Binding Path=Invoices}">
The tooltip should see that Invoices is a bunch of items and use the appropriate data template. The template would look something like this:
<DataTemplate DataType="{x:Type Customer[]}">
<ListBox "ItemsSource={Binding}">
etc
That didn't work so I tried the example from this post x:Type and arrays--how? which involves creating a custom markup extension. This works if you specify the key but not for an implicit template
So then I tried making my own custom markup extension inheriting from TypeExtension like below but I get an error that says "A key for a dictionary cannot be of type 'System.Windows.Controls.StackPanel'. Only String, TypeExtension, and StaticExtension are supported." This is a really weird error as it is taking the content of the datatemplate as the key?? If I specify a key then it works fine but that largely defeats the purpose.
[MarkupExtensionReturnType(typeof(Type)), TypeForwardedFrom("PresentationFramework, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
public class ArrayTypeExtension
: TypeExtension
{
public ArrayTypeExtension() : base() { }
public ArrayTypeExtension(Type type) : base(type)
{
}
public ArrayTypeExtension(string value) : base(value)
{
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
Type val = base.ProvideValue(serviceProvider) as Type;
return val == null ? null : val.MakeArrayType();
}
}

As noted in the question you linked to {x:Type ns:TypeName[]} works. It may screw over the designer but at runtime it should be fine.
To avoid designer errors the template can be moved to App.xaml or a resource dictionary (or of course just don't use the designer at all).
(The error mentioning the control inside the template sounds like a bug in the code generator or compiler, sadly i doubt that there is much you can do about that one.)

If you are OK with creating your own type, I just tried and following and it is working. Create a specific type for your collection:
public class InvoiceCollection : List<Invoice> { }
public class Customer {
public string name { get; set; }
InvoiceCollection invoices { get; set; }
}
and then the XAML with data template:
<DataTemplate DataType={x:Type InvoiceCollection}>
<ListBox ItemsSource="{Binding}" />
</DataTemplate>
<TextBox Text="{Binding name}" Tooltip="{Binding invoices}" />

Related

ContentControl is failing to bind to DataTemplate

I'm unable to bind the <ContentControl Content={Binding Type}/> to the local resources <DataTemplate> when binding my custom property on my POCO class called Type (I'm hoping it's not down to the bad choice of naming).
ReportItemModel
public class ReportItemModel
{
public ReportItemModel(string name, ReportItemType itemType, Type type)
{
Name = name;
ItemType = itemType;
Type = type;
}
public string Name { get; }
public ReportItemType ItemType { get; }
public Type Type { get; }
}
XAML
<ContentControl Content="{Binding Type}" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type moduleTypes:ReportModule}">
<Label>Test</Label>
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
The Type property is (as you've probably guessed) a System.Type and the output within my WPF application is always the output of ToString() which I'm guessing is down to a failed match of my <DataTemplate>. If I change the ContentControl to Content={Binding} and set the <DataTemplate> to accept the data type of the POCO class ReportItemModel it works as intended. The only difference I can see is that ReportItemModel has been instantiated where as the Type property has not.
The issue was down to the XAML DataType="{x:Type moduleTypes:ReportModule}" calling behind the scenes GetType() on a System.Type which would always return a System.RuntimeType as the object was never instantiated and always (at that point) a System.Type (a "Brain Fart" moment to say the least). I was able to get around the issue by using a ValueConverter on the binding <ContentControl Content={Binding Type} and returning the BaseClass name as a string. Using the BaseClass name, It was easy enough to use a couple ofDataTriggers which changed the ContentTemplate value to one of my matched custom DataTemplate's using a similar method shown here.

Using a collection of an abstract type, or interface, as a Dependency Property of a UWP Templated Control

My model has two classes ItemA and ItemB, which implement the ICustomControlItem interface, and they are implemented as follows...
public interface ICustomControlItem
{
string Text { get; set; }
}
public class ItemA : ICustomControlItem
{
public string Text { get; set; }
}
public class ItemB : ICustomControlItem
{
public string Text { get; set; }
}
My objective is to create a templated control CustomControl which has a (dependency) property Items which would be an ObservableCollection<ICustomControlItem>. An ObservableCollection<T> was used as I wanted the view updating when the collection changed.
Accordingly, the control is defined as follows...
[ContentProperty(Name = nameof(Items))]
public sealed class CustomControl : Control
{
public CustomControl()
{
DefaultStyleKey = typeof(CustomControl);
Items = new ObservableCollection<ICustomControlItem>();
}
public ObservableCollection<ICustomControlItem> Items
{
get { return (ObservableCollection<ICustomControlItem>)GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
}
public static readonly DependencyProperty ItemsProperty =
DependencyProperty.Register(nameof(Items), typeof(ObservableCollection<ICustomControlItem>), typeof(CustomControl), new PropertyMetadata(new ObservableCollection<ICustomControlItem>()));
}
...with its XAML ControlTemplate containing a ListView to display the items of the Items dependency property...
<ControlTemplate TargetType="local:CustomControl">
<Grid>
<ListView ItemsSource="{TemplateBinding Items}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</ControlTemplate>
I used the following XAML to initialize and populate (with objects of both ItemA and ItemB classes) an instance of the CustomControl on a XAML Page.
<local:CustomControl>
<local:ItemA Text="Item #1" />
<local:ItemB Text="Item #2" />
<local:ItemA Text="Item #3" />
</local:CustomControl>
At this point I expected all to be well, since both ItemA and ItemB implement the ICustomControlItem interface. However, Visual Studio keeps giving me a warning that reads...
A value of type 'ItemA' cannot be added to a collection or dictionary of type 'ObservableCollection'
However, despite the errors, the XAML renders properly in the designer, and the application runs fine, but IntelliSense doesn't work in the XAML presumed by VS to be erred.
I doubt the issue is with the usage of the ObservableCollection because the errors don't occur when,
Items is an ObservableCollection<object> (however I need it to be constrained to the ICustomControlItem interface).
Items is an ObservableCollection<string> and <x:String> elements are added to the control.
How would this issue be resolved, and a dependency property that is a collection of an abstract type, or interface, be implemented?
Seemingly, the issue was a bug with the version of Visual Studio 2017 Community Edition (or one of its modules) that I had installed then. Updating the IDE (to Version 15.2 (26430.14) Release) resolved the issue.

Custom TabItem properties (TabItem.Content and TabItem.Header)

I'm trying to inherit MyTabItem from System.Windows.Controls.TabItem class. The problem is, that original TabItem has properties of generic object type:
public object Header;
public object Content;
I'm trying to hide those properties in my derived class with different types.
public class MyTabItem: TabItem
{
public new MyTabHeader Header;
public new MyTabContent Content;
}
This way, I can access MyTabItem.Header and MyTabItem.Content without type casting.
The idea is pretty decent and code compiles correctly. However when application starts I see empty controls (no error is reported). When I remove those lines and use base class properties, it works fine.
Of course I could add two additional properties which would internally return casted (MyTabHeader)Header or (MyTabContent)Header, but it seems to be a little redundant.
I'm asking if there is any other way to correctly implement those properties, so they actually work in my application.
This runs completely counter to how WPF was designed to be used. Your XAML objects are supposed to be loosely bound to data, in the vast majority of cases you shouldn't even need to create a custom control. The fact that you are doing this, and then trying to replace the members with type-safe versions of your own, means your view code and your view logic code are no longer separated, and that is going to create you a world of headache down the track.
If you need dynamic tabbing then one way to do it is to first declare an abstract class representing your pages and to derive your page types from it:
public interface IBasePage
{
string Header { get; }
}
public class MyPageA : ViewModelBase, IBasePage
{
public string Header { get { return "Page A"; } }
}
public class MyPageB : ViewModelBase, IBasePage
{
public string Header { get {return "Page B";} }
}
public class MyPageC : ViewModelBase, IBasePage
{
public string Header { get {return "Page C";} }
}
Your view model (which is what your window DataContext should be set to) should then contain a collection of the tabbed pages you wish to display:
public class MyViewModel : ViewModelBase
{
private IEnumerable<IBasePage> _MyPages = new List<IBasePage>(){
new MyPageA(),
new MyPageB(),
new MyPageC()
};
public IEnumerable<IBasePage> MyPages {get {return this._MyPages;}}
}
The tab control in your XAML is then loosely bound to this and should contain a style for your TabItem (so it know what text to use for the header etc) and DataTemplates so that it knows how to render each of the page types you've created:
<TabControl ItemsSource="{Binding MyPages}" SelectedItem="{Binding MyPages[0], Mode=OneTime}">
<TabControl.Resources>
<!-- TabItem style -->
<Style TargetType="{x:Type TabItem}">
<Setter Property="Header" Value="{Binding Header}" />
</Style>
<!-- Content templates -->
<DataTemplate DataType="{x:Type local:MyPageA}">
<TextBlock Text="This is page A" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:MyPageB}">
<TextBlock Text="This is page B" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:MyPageC}">
<TextBlock Text="This is page C" />
</DataTemplate>
</TabControl.Resources>
</TabControl>
The end result is a regular tab control that is completely data driven and is bound to your already-strongly-typed models:

How to format a string in XAML without changing viewmodel's property getter?

I have in my application the following interface:
public interface IContactMedium
{
string ContactString { get; set; }
string Type { get; set;}
bool IsValid();
}
This interface is for objects that represent some sort of contact for a person. It could be a phone, email, etc. The ContactString property is the actual contact data (for a phone, for example, it would be the phone number), and the Type is for differentiation in case a person has more than one (for phone, a person can have a Home phone, a Work phone, Cell phone, etc.) The IsValid method is a validation mechanism for each different type of contact medium.
so, let's say I have two objects in my application - Email and Phone - both implement the interface. I'm going to make in the application a UserControl that holds a UI that manages a list of such objects. So the viewmodel would look something like this:
public class ContactsCollectionViewModel<T> : ViewModelBase where T : class, IContactMedium
{
private ObservableCollection<T> _itemsCollection;
public ContactCollectionViewModel(ObservableCollection<T> items)
{
ItemsCollection = items;
}
public ObservableCollection<T> ItemsCollection
{
get { return _itemsCollection; }
set
{
if (_itemsCollection != value)
{
_itemsCollection = value;
OnPropertyChanged(() => ItemsCollection);
}
}
}
}
I want to add to the IContactMedium interface another property/method that provides proper formatting for the ContactString property when used in Binding in WPF. The idea is that the format in the text box bound to ContactString differs depending on the concrete object that is actually stored in the collection:
<TextBox x:Name="ContactString"
Text="{Binding ContactString, StringFormat=???}" />
I searched online a solution for this and couldn't find anything. I saw people suggesting modifying the ContactString property so the getter returns a formatted value. So, for the Phone object, for example, the property would look like this:
public string ContactString
{
get
{
return string.Format("({0}) {1}-{2}", _contactString.Substring(0,3), _contactString.Substring(4,3), _contactString.Substring(7,3));
}
set {
_contactString = value;
}
}
However, this is not a good solution for me. The information is not only used by the UI. It is also sent to other parts of the application, including a database, that need the phone number in its raw form: ##########.
Is there a way to provide the XAML a formatter to use in the StringFormat attribute of the binding? Can the formatting be dictated by the object that implement the interface? If yes, what type does it need to be, and how can I make it accessible to the Binding in XAML?
Can the formatting be dictated by the object that implement the interface?
In Xaml one can provide data templates which are associated with a specific class.
Simply provide the structure in the template with a formatting on the binding to the target property as shown below:
<Grid>
<Grid.Resources>
<DataTemplate DataType="{x:Type c:Ship}">
<TextBlock Text="{Binding Path=Name, StringFormat=Ship: {0}}"
Foreground="Red" />
</DataTemplate>
<DataTemplate DataType="{x:Type c:Passage}">
<TextBlock Text="{Binding Path=Name, StringFormat=Passage: {0}}"
Foreground="Blue" />
</DataTemplate>
</Grid.Resources>
<ListBox Name="myListBox"
Height="300"
Width="200"
ItemsSource="{Binding OBSCollection}">
</ListBox>
</Grid>
So for my collection where both class instances of Ship and Passage adhere to ITreeEntity:
public ObservableCollection<ITreeEntity> OBSCollection ...
When bound creates a list where the binding has a specific string format as such:
Note in setting up the data the ships were added first followed by the passages. Xaml is not ordering them in anyway.
Need to list different types objects in one ListBox from a composite collection? See my answers here:
Composite Collection ListBox Answer
Basic example of Listbox & Templates
The thing is that each concrete class that implements the interface would have different formatting rules
Can the formatting be dictated by the object that implement the interface?
The dilema is, whether to add the formating logic to your business objects (IContactMedium implementations) or to presentation layer.
if it is business logic, then yes, you should add the formatting code to your business object.
But most probably it is presentation logic. In that case, either create DataTemplate foreach implementation of the IContactMedium, or create converter. In the converter you can choose correct formatting based on the value type. If the output is just plain text, use converter. If its more that plain text, e.g formatted text, use datatemplates.
TIP: You can use unit tests to test if all implementations of IContactMedium have its DataTemplate or are covered by the converter.
You can use converters. Keep your property simple.
public string ContactString { get; set; }
Implement converter
class MyConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
contactString = value as string;
if(contactString == null)
{
throw new InvalidArgumentException();
}
return string.Format("({0}) {1}-{2}",
contactString.Substring(0,3), contactString.Substring(4,3),
contactString.Substring(7,3));
}
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Add it as resource
<Window.Resources>
<local:MyConverter x:Key="MyConverter"/>
</Window.Resources>
Use it
<TextBox x:Name="ContactString"
Text="{Binding ContactString, Converter{StaticResource MyConverter}}" />
You can simply override the ToString() method. By default, a ListBox will use the object's ToString() method as the display text for the item.
public override string ToString()
{
return string.Format("({0}) {1}-{2}", _contactString.Substring(0,3), _contactString.Substring(4,3), _contactString.Substring(7,3));
}
This means you don't have to do anything fancy in the ListBox, like defining a DataTemplate, as the ListBox will pick up the formatted string automatically.
<ListBox Name="myListBox"
Height="300"
Width="200"
ItemsSource="{Binding OBSCollection}"/>

WPF - Binding class with Lists of strings to a ListBox

I have a problem with binding multiple Lists to a ListBox. I want that every List has a different DataTemplate with a different color.
I have following model classes
public class Users
{
public Members Members{ get; set; }
}
public class Members
{
public List<string> Moderators { get; set; }
public List<string> Viewers { get; set; }
}
I have following ViewModel with INotifyPropertyChanged
private Users users;
public Users Users
{
get { return users; }
set
{
users= value;
RaisePropertyChanged("Users");
}
}
And I'm binding to this ListBox
<ListBox ItemsSource="{Binding Users.Members.Viewers}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Now I only have that one List bound to the ListBox. It works great but I want the other list also bound to the same ListBox. Besides that I want that Moderators have a different template.
I tried many different things but nothing seemed to work.
Instead of removing the names from the origination object why not keep it and specify different colors based off of the originating class?
Besides that I want that Moderators have a different template.
If you only have strings that is impossible. Remember the listbox ultimately sees only one list; so in one list, how is it possible to tag a string as either moderator or viewer?
a different DataTemplate with a different color.
If there are only strings I suggest you create wrapper classes, one for moderators and one for viewers, then project the strings into those classes to be held. Then you can follow my suggestion/example below.
Via the use of the Composite collection to hold different items (or one could actually use a base class list or a interface list if the instances have that commonality) and then have specialized data templates which look for the originating class, it can be done.
Example
I have two classes one named Ships and one named Passage. Note that both classes both have a Name property, but one could use something other than Name for either or both in the templates.
Below I define the data templates and my listbox.
<Grid>
<Grid.Resources>
<DataTemplate DataType="{x:Type c:Ship}">
<TextBlock Text="{Binding Path=Name}"
Foreground="Red" />
</DataTemplate>
<DataTemplate DataType="{x:Type c:Passage}">
<TextBlock Text="{Binding Path=Name}"
Foreground="Blue" />
</DataTemplate>
</Grid.Resources>
<ListBox Name="myListBox"
Height="300"
Width="200"
ItemsSource="{Binding MyCompositeCollection}">
</ListBox>
</Grid>
So what will happen is that my ships will be red and the passages will be blue.
Here is my code in the VM:
private CompositeCollection _MyCompositeCollection;
public CompositeCollection MyCompositeCollection
{
get { return _MyCompositeCollection; }
set { _MyCompositeCollection = value; OnPropertyChanged("MyCompositeCollection"); }
}
Here I load the composite collection:
var temp = new CompositeCollection();
Ships.ForEach(sh => temp.Add(sh));
Passages.ForEach(ps => temp.Add(ps));
MyCompositeCollection = temp;
In order to combine two Lists and set it to ItemsSource use CompositeCollection.
WPF can set distinct template by using ItemTemplateSelector but it entails class to be diffrent in some way. Your type is string so it does not differ in any way. My hint is to create enum as follows
enum MemberType
{
Moderator,
Viewer
}
and following class:
class Person
{
public string Name{get;set;}
public MemberType Type{get;set;}
}
then change to this
public class Members
{
public List<Person> Moderators { get; set; }
public List<Person> Viewers { get; set; }
}
and eventually in ItemTemplateSelector
public class TemplateSelector : DataTemplateSelector
{
public DataTemplate ViewerDataTemplate;
public DataTemplate ModeratorDataTemplate;
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var member = item as Person;
switch (member.Type)
{
case MemberType.Moderator:
return ModeratorDataTemplate;
case MemberType.Viewer:
return ViewerDataTemplate;
}
return null;
}
}

Categories