I'm trying to create a TemplateSelector which recognizes if an implements an interface and applies a DataTemplate for it.
I'd like to use this selector in following way:
<ListView Grid.Column="0"
ItemsSource="{Binding Media}"
SelectionMode="None">
<ListView.ItemTemplateSelector>
<selectors:InterfaceAwareTemplateSelector>
<DataTemplate x:Key="IMedia">
<Image Source="{Binding PreviewImage}" />
</DataTemplate>
<DataTemplate x:Key="IDocument">
<TextBlock Text="test" />
</DataTemplate>
</selectors:InterfaceAwareTemplateSelector>
</ListView.ItemTemplateSelector>
</ListView>
I end up with following implementation:
[ContentProperty(Name = "Items")]
public class InterfaceAwareTemplateSelector: DataTemplateSelector {
public DataTemplate DefaultTemplate { get; set; }
public Dictionary<Type, DataTemplate> Items { get; set; }
public InterfaceAwareTemplateSelector() {
Items = new Dictionary<Type, DataTemplate>();
}
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
var result = (
from t in Items
where t.Key.GetTypeInfo().IsAssignableFrom(item.GetType().GetTypeInfo())
select t.Value).FirstOrDefault();
return result ?? DefaultTemplate;
}
}
It of course doesn't work, otherwise I wouldn't write this question :) Application crushes with a message a xaml cannot be parsed:
A first chance exception of type 'Windows.UI.Xaml.Markup.XamlParseException' occurred in Hicron.ProductCatalog.MainUI.exe
WinRT information: E_UNKNOWN_ERROR [Line: 47 Position: 39]
An exception of type 'Windows.UI.Xaml.Markup.XamlParseException' occurred in Hicron.ProductCatalog.MainUI.exe but was not handled in user code
WinRT information: E_UNKNOWN_ERROR [Line: 47 Position: 39]
Additional information: Unspecified error
What's wrong with that dictionary? Normally I'd use CompositeCollection and merge multiple sources but this class is missing in WinRT :(
EDIT
In terms of fixing dictionary problem I've changed dictionary to list of custom types. Still can't create a custom type with Type set from XAML. I could use a string but than I can't manage it in code unless I specify fully qualified type name.
[ContentProperty(Name = "Items")]
public class InterfaceAwareTemplateSelector: DataTemplateSelector {
public DataTemplate DefaultTemplate { get; set; }
public List<InterfaceAwareTemplateSelectorItem> Items { get; set; }
public InterfaceAwareTemplateSelector() {
Items = new List<InterfaceAwareTemplateSelectorItem>();
}
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) {
if (item == null) {
return DefaultTemplate;
}
var result = (
from t in Items
where t.Type.GetTypeInfo().IsAssignableFrom(item.GetType().GetTypeInfo())
select t.Template).FirstOrDefault();
return result ?? DefaultTemplate;
}
}
public class InterfaceAwareTemplateSelectorItem
{
public Type Type { get; set; }
public DataTemplate Template { get; set; }
}
Corresponding XAML:
// somewhere in page tag
xmlns:bo="using:/*long long namespace*/.BusinessObjects"
// somewhere in XAML file
<ListView Grid.Column="0"
ItemsSource="{Binding Media}"
SelectionMode="None">
<ListView.ItemTemplateSelector>
<selectors:InterfaceAwareTemplateSelector>
<selectors:InterfaceAwareTemplateSelectorItem Type="bo:IMedia">
<selectors:InterfaceAwareTemplateSelectorItem.Template>
<DataTemplate>
<Image Source="{Binding PreviewImage}"
Tapped="ImageTapped" />
</DataTemplate>
</selectors:InterfaceAwareTemplateSelectorItem.Template>
</selectors:InterfaceAwareTemplateSelectorItem>
<selectors:InterfaceAwareTemplateSelectorItem Type="bo:IDocument">
<selectors:InterfaceAwareTemplateSelectorItem.Template>
<DataTemplate>
<TextBlock Text="pa8u4mrapwu" />
</DataTemplate>
</selectors:InterfaceAwareTemplateSelectorItem.Template>
</selectors:InterfaceAwareTemplateSelectorItem>
</selectors:InterfaceAwareTemplateSelector>
</ListView.ItemTemplateSelector>
</ListView>
Okay, so using this:
public interface IFake1 { }
public interface IFake2 { }
public class TemplateItem
{
public DataTemplate Template { get; set; }
public string Interface { get; set; }
}
public class MySelector : DataTemplateSelector
{
public List<TemplateItem> Templates { get; set; }
}
I could do this:
<GridView>
<GridView.ItemTemplateSelector>
<local:MySelector>
<local:MySelector.Templates>
<local:TemplateItem Interface="IFake1">
<local:TemplateItem.Template>
<DataTemplate>
<!-- TODO -->
</DataTemplate>
</local:TemplateItem.Template>
</local:TemplateItem>
<local:TemplateItem Interface="IFake2">
<local:TemplateItem.Template>
<DataTemplate>
<!-- TODO -->
</DataTemplate>
</local:TemplateItem.Template>
</local:TemplateItem>
</local:MySelector.Templates>
</local:MySelector>
</GridView.ItemTemplateSelector>
</GridView>
The error appears in the Type you are using. I could not get that to work. Had to use String. Should be simple to parse form there.
Best of luck!
See if it works if you replace Dictionary<Type, DataTemplate> with ResourceDictionary. I'm betting at least one of the problems is that the key in x:Key="IMedia" can't be implicitly converted to Type. You could also just try using string as the key type.
I finally managed to fix it. Unfortunately I couldn't get a conversion from string to Type in XAML so I had to stick to strings :/ Not very convenient but at least works. That's what I ended up with:
XAML
<ListView Grid.Column="0"
ItemsSource="{Binding Media}"
SelectionMode="None">
<ListView.ItemTemplateSelector>
<selectors:InterfaceAwareTemplateSelector>
<DataTemplate x:Key="IMedia">
<Image Source="{Binding PreviewImage}" Tapped="ImageTapped"/>
</DataTemplate>
<DataTemplate x:Key="IDocument">
<commonItems:DocumentItemPresenter TappedCommand="{Binding DataContext.OpenDocument, ElementName=PageRoot}"/>
</DataTemplate>
</selectors:InterfaceAwareTemplateSelector>
</ListView.ItemTemplateSelector>
</ListView>
Selector itself:
[ContentProperty(Name = "Items")]
public class InterfaceAwareTemplateSelector: DataTemplateSelector {
public DataTemplate DefaultTemplate { get; set; }
public Dictionary<string, DataTemplate> Items { get; set; }
public InterfaceAwareTemplateSelector() {
Items = new Dictionary<string, DataTemplate>();
}
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) {
if (item == null) {
return DefaultTemplate;
}
var result = (
from ii in item.GetType().GetTypeInfo().ImplementedInterfaces
from dt in Items
where ii.Name == dt.Key
select dt.Value).FirstOrDefault();
return result ?? DefaultTemplate;
}
}
I upvoted Filip & Jerry because I found your tips helpful. Thank you guys.
If anyone is interested how this problem was solved below is final implementation and a usage example.
Usage:
<GridView ....>
<GridView.ItemTemplateSelector>
<selectors:InterfaceAwareTemplateSelector>
<!-- ReSharper disable once Xaml.RedundantResource -->
<DataTemplate x:Key="INewsContainer" selectors:InterfaceAwareTemplateSelector.Priority="1">
<ctrls:ItemsContainerTile Width="350" Height="350"
ItemTappedCommand="{Binding DataContext.OpenNewsDetails, ElementName=PageRoot}"/>
</DataTemplate>
<!-- ReSharper disable once Xaml.RedundantResource -->
<DataTemplate x:Key="ISimpleMaterial" selectors:InterfaceAwareTemplateSelector.Priority="0">
<ctrls:GenericTile Width="350" Height="350"
TappedCommand="{Binding DataContext.OpenDetails, ElementName=PageRoot}" />
</DataTemplate>
</selectors:InterfaceAwareTemplateSelector>
</GridView.ItemTemplateSelector>
.... the rest of XAML
The priority is to control the order in which datatamples should be checked. This way we can control what happens if multiple keys matches the object being converted.
Implementation:
[ContentProperty(Name = "Items")]
public class InterfaceAwareTemplateSelector: DataTemplateSelector {
public DataTemplate DefaultTemplate { get; set; }
public Dictionary<string, DataTemplate> Items { get; set; }
public InterfaceAwareTemplateSelector() {
Items = new Dictionary<string, DataTemplate>();
}
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) {
if (item == null) {
return DefaultTemplate;
}
var results = (
from ii in item.GetType().GetTypeInfo().ImplementedInterfaces
from dt in Items
where ii.Name == dt.Key
select dt)
.ToArray();
if (results.Length > 1) {
var orderedResults =
from r in results
where IsPrioritySet(r.Value)
orderby GetPriority(r.Value) descending
select r;
if (orderedResults.Any()) {
return orderedResults.First().Value;
}
throw new AmbigiousResolveTemplateFound(item.GetType(), results.Select(x => x.Key));
}
else if (results.Length == 1) {
return results[0].Value;
}
return DefaultTemplate;
}
#region PriorityProperty
public static readonly DependencyProperty PriorityProperty =
DependencyProperty.RegisterAttached(
"Priority",
typeof(int),
typeof(InterfaceAwareTemplateSelector),
new PropertyMetadata(0));
public static int GetPriority(DependencyObject item) {
if (item == null) { throw new ArgumentNullException("item"); }
return (int)item.GetValue(PriorityProperty);
}
public static void SetPriority(DependencyObject item, int value) {
if (item == null) { throw new ArgumentNullException("item"); }
item.SetValue(PriorityProperty, value);
}
public static bool IsPrioritySet(DependencyObject item) {
if (item == null) { throw new ArgumentNullException("item"); }
var result = item.ReadLocalValue(PriorityProperty);
return result != DependencyProperty.UnsetValue;
}
#endregion
}
Hope someone will find this implementation helpful. Once again, many thanks to Filip and Jerry for your help.
Related
MAUI.NET
I have a special picker for my own that I want to reuse in many places
<ContentView ...
x:Class="XYZ.Views.ABCPicker"
x:DataType="views:ABCPicker">
<Grid>
<Picker ItemsSource="{Binding ...}" SelectedItem="{Binding ChoosenDeviceTypeProperty}"/>
</Grid>
</ContentView>
and its code behind with my AttachedProperty:
public partial class ABCPicker : ContentView
{
public static readonly BindableProperty ChoosenDeviceTypeProperty = BindableProperty.CreateAttached("ChoosenDeviceType", typeof(string), typeof(ABCPicker), "Detect by connect", BindingMode.TwoWay);
public static string GetChoosenDeviceType(BindableObject view)
{
return (string)view.GetValue(ChoosenDeviceTypeProperty);
}
public static void SetChoosenDeviceType(BindableObject view, string value)
{
view.SetValue(ChoosenDeviceTypeProperty, value);
}
public ABCPicker()
{
this.BindingContext = this;
InitializeComponent();
}
}
I want to consume it in my broader control ViewModel
public class BroaderControlViewModel : BaseViewModel
{
...
private string myResult;
public string MyResult
{
get { return myResult; }
set { SetPropertyAndNotify(ref myResult, value); }
}
public ICommand MyCommand { private set; get; }
public BroaderControlViewModel()
{
MyCommand = new Command(() =>
{
// here I want the to get a choosen value to proceed with it after the user have choosen value in my special picker
});
}
}
with a view like below:
<ContentView ...
x:Class="XYZ.Views.BroaderControlView"
x:DataType="viewModels:BroaderControlViewModel">
<VerticalStackLayout ...>
<views:ABCPicker ChoosenDeviceType="{Binding MyResult, Mode=TwoWay}" />
<Button Text="Connect" Command="{Binding MyCommand}"/>
</VerticalStackLayout>
</ContentView>
I tried different binding types, also with x:Reference this and also with normal binding properties - not attached ones.
How it should be matched?
I have this simple ListView filled from an ObservableCollection. Once the list is bound, I would like to access the parent vm view model from inside this ItemTemplate so that I can bind the command called cmd_delete_mesh. How is this done for a UWP Xaml app (not wpf)?
<ListView x:Name="mesh_list" SelectedItem="{x:Bind vm.selected_mesh, Mode=TwoWay}" ItemsSource="{x:Bind vm.meshes}">
<ListView.ItemTemplate>
<DataTemplate>
<ListViewItem>
<Button Command="{Binding cmd_delete_mesh}"/>
You can do like so:
<ListView x:Name="mesh_list" SelectedItem="{x:Bind vm.selected_mesh, Mode=TwoWay}" ItemsSource="{x:Bind vm.meshes}">
<ListView.ItemTemplate>
<DataTemplate>
<ListViewItem>
<Button Command="{Binding ElementName=mesh_list, Path=DataContext.vm.cmd_delete_mesh}"/>
I do this from code unfortunately... I’ll post an example of my code soon
You could define your command in your model and declare an event in it. In your ViewModel, when you initialize the 'meshes' collection, you could register this event for every item in this collection. Then, when the command is executed, you just need to raise the event and do some operations in its event handler.
I made a simple code sample for your reference:
<ListView x:Name="mesh_list" SelectedItem="{x:Bind ViewModel.selected_mesh, Mode=TwoWay}" ItemsSource="{x:Bind ViewModel.meshes,Mode=OneWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:SubTest">
<ListViewItem>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{x:Bind Name }"></TextBlock>
<Button Command="{x:Bind cmd_delete_mesh}" Content="delete"/>
</StackPanel>
</ListViewItem>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
ViewModel = new Test("test data");
}
private Test ViewModel { get; set; }
}
public class Test : ViewModelBase
{
public string Name { get; set; }
private SubTest _selected_mesh;
public SubTest selected_mesh
{
get { return _selected_mesh; }
set
{
if (_selected_mesh != value)
{
_selected_mesh = value;
RaisePropertyChanged("selected_mesh");
}
}
}
public ObservableCollection<SubTest> meshes { get; set; } = new ObservableCollection<SubTest>();
public Test(string name)
{
this.Name = name;
for (int i = 0; i < 10; i++)
{
var sub = new SubTest() { Name = "String " + i };
sub.DeleteParentItem += Test_DeleteParentItem;
meshes.Add(sub);
}
}
private void Test_DeleteParentItem()
{
if (selected_mesh != null)
{
DeleteItem(selected_mesh);
}
}
private void DeleteItem(SubTest subTest)
{
//TODO...
}
}
public class SubTest
{
public RelayCommand cmd_delete_mesh { get; set; }
public string Name { get; set; }
public event Action DeleteParentItem;
public SubTest()
{
cmd_delete_mesh = new RelayCommand(DeleteItem);
}
private void DeleteItem()
{
if (DeleteParentItem != null)
{
DeleteParentItem.Invoke();
}
}
}
Please note that the ViewModelBase and RelayCommand are from mvvmlight.
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
what is the easiest way to bind selected state of listview's item to model's boolean property?
I have model:
class Model {
public string Name { get; set; }
public bool Selected { get; set; }
}
And listview:
<ListView x:Name="myListView" SelectionMode="Multiple">
<ListView.ItemTemplate>
<DataTemplate x:DataType="x:String">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
I bind items to the listview:
myListView.ItemsSource = // list of Model instances
I want the Selected property of Model to always reflect whether it is selected or not in myListView. So - by selecting/deselecting the item in myListView, it will hold the apropriate bool value or by setting Selected property myListView will select/deselect appropriate item.
what is the easiest way to bind selected state of listview's item to model's boolean property?
I'm not sure if this is the most easiest way, but for me I think it is the easiest way to bind SelectorItem.IsSelected property of ListViewItem to your Selected property in model. Only the problem is, we all know each item of ListView is an instance of ListViewItem, but when we use DataTemplate to build the item structure for ListViewItem, ListViewItems are not available in design-time. So my idea is to bind this property in code behind, just for example here:
<ListView x:Name="myListView" SelectionMode="Multiple" Loaded="myListView_Loaded" ItemsSource="{x:Bind Collection}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
code behind:
private ObservableCollection<Model> Collection = new ObservableCollection<Model>();
protected override void OnNavigatedTo(NavigationEventArgs e)
{
for (int i = 0; i < 100; i++)
{
Collection.Add(new Model { Name = "Name " + i });
}
}
private void myListView_Loaded(object sender, RoutedEventArgs e)
{
IEnumerable<ListViewItem> lvItems = FindVisualChildren<ListViewItem>(myListView);
if (lvItems != null)
{
foreach (ListViewItem lvitem in lvItems)
{
Model model = lvitem.Content as Model;
Binding b = new Binding
{
Source = model,
Path = new PropertyPath("Selected"),
Mode = BindingMode.TwoWay,
};
BindingOperations.SetBinding(lvitem, ListViewItem.IsSelectedProperty, b);
}
}
}
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
yield return (T)child;
}
foreach (T childOfChild in FindVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}
public class Model : INotifyPropertyChanged
{
public string Name { get; set; }
private bool _Selected;
public bool Selected
{
get { return _Selected; }
set
{
if (value != _Selected)
{
_Selected = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName]string propertyName = "")
{
if (this.PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I think the perfect way would be to implement the ListView.ItemSelectionChanged event, and iterate through each item in the list view model to set it to true for the item which is selected, false for the rest items.
However, You may want to try something like this, although honestly I'm not sure if this is the correct way:
class Model {
public string Name { get; set; }
public bool Selected
{
get
{
return MyListView.SelectedItems.Count(x => x.Name == Name) > 0;
}
}
}
So I'm trying to implement a ListBoxItem that has a TextBlock that binds to a property in the viewmodel. But for some reason the view isn't being updated even after the viewmodel property is updated. Can anyone give me some pointers?
In the view (xaml)
<phone:Pivot HorizontalAlignment="Left" Height="748" Margin="10,10,0,0" Grid.Row="1" Title="pivot" VerticalAlignment="Top" Width="460">
<StackPanel x:Name="POIStackPanel" DataContext="{StaticResource POIViewModel}">
<ListBoxItem x:Name="SelectedPOIItem">
<TextBlock Text="{Binding SelectedPickupPOI.label}" />
</ListBoxItem>
phone:PivotItem x:Name="GooglePivot" Header="Google">
<ListBox x:Name="GooglePOIList"
ItemsSource="{Binding GooglePOICollection}"
HorizontalAlignment="Left"
Height="441"
Width="436"
SelectedItem="{Binding SelectedPickupPOI, Mode=TwoWay}"
ItemTemplate="{StaticResource POIItemTemplate}" />
</phone:PivotItem>
</StackPanel>
</phone:Pivot>
In the viewmodel
public ObservableCollection<PointOfInterest> GooglePOICollection { get; private set; }
public PointOfInterest SelectedPickupPOI
{
get
{
return _selectedPickupPOI;
}
set
{
_selectedPickupPOI = value;
NotifyPropertyChanged("SelectedPickupPOI");
}
}
public PointOfInterestViewModel()
{
this.GooglePOICollection = = new ObservableCollection<PointOfInterest>();
this.SelectedPickupPOI = new PointOfInterest();
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
My PointOfInterest Models
public abstract class PointOfInterest : INotifyPropertyChanged
{
public abstract string id { get; set; }
public abstract string label { get; set; }
}
public class GooglePOI : PointOfInterest
{
private string _label;
public override string label
{
get { return _label; }
set
{
_label = value;
NotifyPropertyChanged("label");
}
}
}
Update
so I found out that if I do _selectedPickupPOI.label = value.label; it will update the view correctly. What am I doing wrong?
I'm trying to populate a list view control on a XAML page in a Win8 application. I've added the following attributes to the page XAML:
<common:LayoutAwarePage x:Class="SecAviTools.ViewWeatherHome"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:common="using:MyNameSpace.Common"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:MyNameSpace"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewmodel="using:MyNameSpace.Win8ViewModel"
x:Name="pageRoot"
DataContext="{Binding DefaultViewModel,
RelativeSource={RelativeSource Self}}"
mc:Ignorable="d">
<!-- ... -->
<ListView ItemsSource="{Binding Path=viewmodel:Stations}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Id}"/>
<TextBlock Text="{Binding Path=Name}"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
My source class is:
namespace MyNameSpace.Win8ViewModel
{
public class Stations : INotifyPropertyChanged, INotifyCollectionChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
protected void OnCollectionChanged<T>(NotifyCollectionChangedAction action, ObservableCollection<T> items)
{
if (CollectionChanged != null)
CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, items));
}
public Stations()
{
AllStations = new ObservableCollection<Station>();
AddStations(new List<Station>());
}
public ObservableCollection<Station> AllStations { get; private set; }
public void AddStations(List<Station> stations)
{
AllStations.Clear();
foreach (var station in stations)
AllStations.Add(station);
OnCollectionChanged(NotifyCollectionChangedAction.Reset, AllStations);
OnPropertyChanged("AllStations");
}
}
public class Station
{
public int Id { get; set; }
public string Name { get; set; }
}
}
There is also a button on the page (not shown here) that does the following:
public sealed partial class MyPage : MyNameSpace.Common.LayoutAwarePage
{
private Stations m_Stations = new Stations();
//...
private async void SearchButtonClick(object sender, RoutedEventArgs e)
{
var list = new List<Station>();
list.Add(new Station() { Id = 0, Name = "Zero" });
list.Add(new Station() { Id = 1, Name = "One" });
m_Stations.AddStations(list);
}
}
However, when I run the code, nothing appears in the list view. What am I missing?
TIA
You don't show what DefaultViewModel is, but I'll assume it's set to be an instance of the class you show, Stations. In that case, you need to binding to be:
<ListView ItemsSource="{Binding Path=AllStations}">
The Path of a binding usually refers to a property somewhere; with no further specification, such as a Source, it's a property on the object that is set to be the DataContext.
Regardless, you don't need the namespace qualifier "viewmodel:".
As an aside, if you do end up binding to the ObservableCollection, you don't need to implement INotifyCollectionChanged, only INotifyPropertyChanged for when the AllStations property is set.