How to get a value from custom control in a broader control via binding in VM - xaml

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?

Related

How do I bind a value to a referenced custom content page (XAML)

I created a custom ContentPage to show a circle avatar with initials. When I pass a value via binding it comes up as null, I debugged to check this. Can someone help me please? Thanks.
HomePage.xaml
<views:CircleView CircleText="{Binding Initials}"/>
CircleView.xaml
<Frame x:Name="circleFrame">
<Label x:Name="circleLabel"
Text="{Binding CircleText}"/>
</Frame>
CircleView.xaml.cs
public partial class CircleView : ContentView
{
public CircleView()
{
InitializeComponent();
BindingContext = this;
}
public static readonly BindableProperty CircleTextProperty =
BindableProperty.Create(nameof(CircleText), typeof(string), typeof(CircleView), null);
public string CircleText
{
get { return (string)GetValue(CircleTextProperty); }
set { SetValue(CircleTextProperty, value); }
}
}
You could try the code below.
CircleView.xaml:
<Frame x:Name="circleFrame">
<Label x:Name="circleLabel"/>
</Frame>
CircleView.xaml.cs:
public CircleView()
{
InitializeComponent();
}
public static readonly BindableProperty CircleTextProperty =
BindableProperty.Create(nameof(CircleText), typeof(string), typeof(CircleView), propertyChanged:(b,o,n)=>(b as CircleView).OnChanged());
private void OnChanged()
{
circleLabel.Text = CircleText;
}
public string CircleText
{
get { return (string)GetValue(CircleTextProperty); }
set { SetValue(CircleTextProperty, value); }
}
MainPage.xaml:
<views:CircleView CircleText="{Binding Initials}"/>
MainPage.xaml.cs:
public string Initials { get; set; }
public MainPage()
{
InitializeComponent();
Initials = "Hello";
this.BindingContext = this;
}
In CircleView.xaml, you should include a source to the binding, would be something like:
<ContentView x:Name="Self">
<Frame x:Name="circleFrame">
<Label x:Name="circleLabel"
Text="{Binding source={x:reference Self}, Path=CircleText}"/>
</Frame>
</ContentView>

Xamarin.Forms binding to a binding property or something

I have a page
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:PassSystem.Controls;assembly=PassSystem"
x:Class="PassSystem.Views.CreatePassPage"
Title="Оформление пропуска">
<ScrollView HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<StackLayout>
<FlexLayout Direction="Column" BackgroundColor="White">
<controls:ActionOption Title="Название" Value="1" LeftMargin="18">
</controls:ActionOption>
</FlexLayout>
</StackLayout>
</ScrollView>
</ContentPage>
and i have contentview
<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:PassSystem.Controls;assembly=PassSystem"
xmlns:effects="clr-namespace:PassSystem.Effects;assembly=PassSystem"
xmlns:local="clr-namespace:PassSystem"
x:Class="PassSystem.Controls.ActionOption">
<FlexLayout x:Name="MainLayout" Direction="Row" JustifyContent="Center" AlignContent="Center" AlignItems="Center" HeightRequest="52"
effects:ClickableEffect.ClickCommand="!!"
effects:ClickableEffect.CancelCommand="{Binding UpCommand}"
effects:ClickableEffect.UpCommand="{Binding UpCommand}"
effects:ClickableEffect.DownCommand="{Binding DownCommand}">
<Label Text="{Binding Title}"
TextColor="{StaticResource PrimaryTextColor}"
FontSize="15"
VerticalTextAlignment="Center"
VerticalOptions="FillAndExpand"
FlexLayout.Grow="1"
Margin="{Binding TitleMargin, Mode=TwoWay}">
</Label>
<StackLayout Orientation="Horizontal" HeightRequest="16" VerticalOptions="CenterAndExpand" Margin="0,0,8,0">
<Label Text="{Binding Value}" FontSize="12" TextColor="{StaticResource SecondaryTextColor}" VerticalTextAlignment="Center"/>
<controls:IconView Source="forward" HeightRequest="14" WidthRequest="14" ForegroundColor="{StaticResource AdditionalTextColor}"/>
</StackLayout>
<FlexLayout.Effects>
<effects:ClickableEffect />
</FlexLayout.Effects>
</FlexLayout>
</ContentView>
.cs file
public partial class ActionOption : ContentView
{
public ActionOption()
{
InitializeComponent();
BindingContext = this;
}
private string _title;
public string Title
{
get => _title;
set
{
if(value == _title) return;
_title = value;
OnPropertyChanged();
}
}
private string _value;
public string Value
{
get => _value;
set
{
if (value == _value) return;
_value = value;
OnPropertyChanged();
}
}
public Thickness TitleMargin => new Thickness(_leftMargin, 0, 6, 0);
private double _leftMargin;
public double LeftMargin
{
get => _leftMargin;
set
{
if (Math.Abs(value - _leftMargin) < 0.01d) return;
_leftMargin = value;
OnPropertyChanged();
OnPropertyChanged(nameof(TitleMargin));
}
}
private ICommand _clicked;
public ICommand Clicked
{
get => _clicked;
set
{
if (value == _clicked) return;
_clicked = value;
OnPropertyChanged();
}
}
public ICommand UpCommand => new Command(() => MainLayout.BackgroundColor = Color.FromHex("#fff"));
public ICommand DownCommand => new Command(() => MainLayout.BackgroundColor = (Color)((App)Application.Current).Resources["HighlightingColor"]);
}
And I need to bind the ClickableEffect.ClickCommand from the page. I.e.
<controls:ActionOption Title="TitleHere" Value="1" LeftMargin="18" Clicked="{Binding ClickedCommand}">
</controls:ActionOption>
And in control
`effects:ClickableEffect.ClickCommand="{Binding ClickCommand (FromPage)}"`
Additional information. ClickableEffect
{
public ClickableEffect() : base("PassSystem.ClickableEffect")
{
}
#region Click
public static readonly BindableProperty ClickCommandProperty = BindableProperty
.CreateAttached("ClickCommand", typeof(ICommand), typeof(ClickableEffect), (object)null);
public static ICommand GetClickCommand(BindableObject view)
{
return (ICommand)view.GetValue(ClickCommandProperty);
}
public static void SetClickCommand(BindableObject view, ICommand value)
{
view.SetValue(ClickCommandProperty, value);
}
public static readonly BindableProperty ClickCommandParameterProperty = BindableProperty
.CreateAttached("ClickCommandParameter", typeof(object), typeof(ClickableEffect), (object)null);
public static object GetClickCommandParameter(BindableObject view)
{
return view.GetValue(ClickCommandParameterProperty);
}
public static void SetClickCommandParameter(BindableObject view, object value)
{
view.SetValue(ClickCommandParameterProperty, value);
}
#endregion
}
Implementation of Clickable Effect on Android:
public class ClickableListener : Java.Lang.Object, View.IOnTouchListener, View.IOnClickListener, View.IOnLongClickListener
{
private Element Element { get; }
private View View { get; }
public ClickableListener(Element element, View view)
{
Element = element;
View = view;
}
...
public void OnClick(View v)
{
Tap();
}
private void Tap()
{
var command = ClickableEffect.GetClickCommand(Element);
var parameter = ClickableEffect.GetClickCommandParameter(Element);
command?.Execute(parameter);
}
}
[assembly: ResolutionGroupName("PassSystem")]
[assembly: ExportEffect(typeof(AndroidClickableEffect), "ClickableEffect")]
namespace PassSystem.Droid.Native.Effects
{
public class AndroidClickableEffect : PlatformEffect
{
private bool _attached;
protected override void OnAttached()
{
//because an effect can be detached immediately after attached (happens in listview), only attach the handler one time.
if(!_attached)
{
var control = Control ?? Container;
var listener = new ClickableListener(Element, control);
control.SetOnClickListener(listener);
...
_attached = true;
}
}
}
How to implement this? Thank you.
P.S. It's hard for me to explain and I hope you understand. :)
In order to achieve the desired behaviour:
ClickCommand have to be an attached property of a ClickableEffect. More information can be found in the official doc: Passing Effect Parameters as Attached Properties.
Since you have a ContentView in between the effect and the page, you have to make sure that BindindContext of the ContentView is set correctly. I am afraid that you will have to define a ClickCommand bindable property on the ContentView level and bind it to the effect's command. So: Page.BindingContext.ClickCommand => ContentView.ClickCommand => ClickableEffect.ClickCommand (where => is binding).
Which basically raise a question - why do you need an effect?

Xamarin Detecting Enter Key MVVM

How can I detect when the enter key (or any other key) is pressed using a MVVM approach. I'm new to xamarin so I hope I'm asking the right question. My thought would be to add a command to the password Entry. Would that be the right approach? I could add a Completed Event to the back end code but how can I link that to my view model?
Here is my XAML Code
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:newApp"
x:Class="newApp.MainPage">
<ContentPage.Content>
<StackLayout Padding="30" Spacing="10" VerticalOptions="Center">
<Image Source="logo.png" />
<Entry Text="{Binding Username}" Placeholder="Username" />
<Entry Text="{Binding Password}" IsPassword="True" Placeholder="Password" />
<Label Text="{Binding DisplayMessage}"/>
<Button Text="Log In" Command="{Binding LogInCommand}" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
Here is the Back End Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace newApp
{
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
BindingContext = new LoginViewModel();
}
}
}
And Here is my ViewModel
namespace newApp
{
class LoginViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
string username, password;
Boolean bol;
void OnPropertyChanged([CallerMemberName] string name = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public string Password
{
get { return password; }
set
{
password = value;
OnPropertyChanged();
}
}
public string Username
{
get { return username; }
set
{
username = value;
OnPropertyChanged();
OnPropertyChanged(nameof(DisplayMessage));
}
}
public string DisplayMessage
{
get
{
if (username != ""){return $"This is your {Username}";}
else return "";
}
}
void Login()
{
if (username == "")
bol = false;
}
}
}
The short answer is: you can't. The Completed event is just that: an event. Because of the way how events work, they are not suitable for the MVVM pattern.
There is a couple of ways to go about this. First, you could catch the event in your code-behind and then trigger code in your view model that is in your BindingContext property. Although you take a little stray from the MVVM pattern, this is a way around this.
The other option is to create your own inheritance of the control and implement a new property that does take a Command. You can then loop the event internally to the Command.
But probably the easiest solution to this is create a Behavior that turns your event into a Command. To create a reusable Behavior that turns any event on it to a Command, implement it like this (full implementation can be found in the link below):
public class EventToCommandBehavior : BehaviorBase<View>
{
public static readonly BindableProperty EventNameProperty =
BindableProperty.Create ("EventName", typeof(string), typeof(EventToCommandBehavior), null, propertyChanged: OnEventNameChanged);
public static readonly BindableProperty CommandProperty =
BindableProperty.Create ("Command", typeof(ICommand), typeof(EventToCommandBehavior), null);
public static readonly BindableProperty CommandParameterProperty =
BindableProperty.Create ("CommandParameter", typeof(object), typeof(EventToCommandBehavior), null);
public static readonly BindableProperty InputConverterProperty =
BindableProperty.Create ("Converter", typeof(IValueConverter), typeof(EventToCommandBehavior), null);
public string EventName { ... }
public ICommand Command { ... }
public object CommandParameter { ... }
public IValueConverter Converter { ... }
...
}
And for your Entry, attach it like this:
<Entry Text="{Binding Username}">
<Entry.Behaviors>
<local:EventToCommandBehavior EventName="Completed" Command="{Binding CompletedCommand}" />
</Entry.Behaviors>
</Entry>
Read more about this in the docs: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/behaviors/reusable/event-to-command-behavior

Label Text Not Updating From Bindable Property

I have a very simple form that I'm using to experiment with BindableProperty. Here's the XAML for the form
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MyBindableProperty"
x:Class="MyBindableProperty.MainPage">
<StackLayout>
<local:MyLabel x:Name="BindingLabel" Text="{Binding Text}" MyText="{Binding Text}"
VerticalOptions="Center"
HorizontalOptions="Center" />
<Entry x:Name="BindingEntry" Text="{Binding Text, Mode=TwoWay}"/>
<Entry x:Name="BindingEntry2" Text="{Binding Text, Mode=TwoWay}"/>
<Button x:Name="BindingButton" Text="Reset"/>
</StackLayout>
</ContentPage>
And here is the code behind
public partial class MainPage : ContentPage
{
public DataSourceClass DataSourceObject { get; set; }
public MainPage()
{
DataSourceObject = new DataSourceClass { Text = "Test1" };
BindingContext = DataSourceObject;
InitializeComponent();
BindingButton.Clicked += BindingButton_Clicked;
}
private void BindingButton_Clicked(object sender, EventArgs e)
{
var boundText = this.BindingLabel.Text;
var boundMyText = this.BindingLabel.MyText;
DataSourceObject.Text = "Test2";
}
}
Finally, here is the custom label class used in the XAML -
public class MyLabel : Label
{
public string MyText
{
get
{
return (string)GetValue(MyTextProperty);
}
set
{
SetValue(MyTextProperty, value);
}
}
public static readonly BindableProperty MyTextProperty = BindableProperty.Create(nameof(MyText), typeof(string), typeof(MyLabel), "Test", BindingMode.TwoWay, propertyChanged: MyTextChanged);
public static void MyTextChanged(BindableObject bindable, object oldValue, object newValue)
{
((MyLabel)bindable).TextChanged(newValue.ToString());
}
public void TextChanged(string newText)
{
Device.BeginInvokeOnMainThread(() => this.Text = newText);
}
}
The issues I'm having are
when the page initialises the MyTextChanged handler fires, but not after any subsequent changes
when the Reset button is clicked the value in DataSourceObject.Text is correctly updated with the value from the Entry element
no matter how I try to set the values of BindingLabel and BindingEntry2 they never reflect the values of DataSourceObject.Text after the page has loaded.
Any help would be greatly appreciated.
I stumbled across this so I updated the DataSourceClass from this
public class DataSourceClass
{
public string Text { get; set; }
}
to this
public class DataSourceClass : INotifyPropertyChanged
{
private string _text;
public string Text
{
get
{
return _text;
}
set
{
_text = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Text"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
and now everything works.
I thought BindableProperty was meant to supersede INotifyPropertyChanged?

How to add items to Dictionary

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.