Collectionview SelectionChangeCommand doesn´t work with CommunityToolkit.Mvvm .NET MAUI - mvvm

I have a simple project using Community Toolkit Mvvm tool and a collectionview. The issue is that the SelectionChangeCommand of the CollectionView doesn´t fired when I select an element of the collection. I created this project because on another more complex project the error is that it can't find the binding command on the viewmodel. I know that the connection between the view and viewmodel is working because the collectionview is filling with elements on the viewmodel and also I am able to change the visibility of the border through the binded property.
Sample project: https://github.com/luis95gr/MvvmError.App
Code
LoginPage (first page with collectionview)
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MvvmError.Views.LoginPage"
xmlns:models="clr-namespace:MvvmError.Models"
Title="LoginPage">
<RefreshView VerticalOptions="FillAndExpand">
<Border BackgroundColor="Red" IsVisible="{Binding Visible}" Stroke="Red" HorizontalOptions="FillAndExpand" Padding="0,0" Margin="0,0,0,0" VerticalOptions="FillAndExpand">
<Border.StrokeShape>
<RoundRectangle CornerRadius="40,0,0,0" />
</Border.StrokeShape>
<CollectionView Background="Transparent" ItemsSource="{Binding Messages}" SelectionMode="Single" SelectedItem="{Binding MessageSelected}" SelectionChangedCommand="{Binding MessageSelectedCommand}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:MessagesMessages">
<Grid RowDefinitions="100*" ColumnDefinitions="100*">
<Label Grid.Row="0" Text="{Binding Asunto}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Border>
</RefreshView>
LoginPageViewModel
public partial class LoginPageViewModel : ObservableObject
{
[ObservableProperty]
private MessagesMessages messageSelected;
[ObservableProperty]
private bool visible;
public ObservableCollection<MessagesMessages> Messages { get; } = new();
public LoginPageViewModel()
{
Visible = true;
GetMessages();
}
[RelayCommand]
async void MessageSelectedCommand()
{
var snackbar = Snackbar.Make("Hola");
await snackbar.Show();
}
private void GetMessages()
{
Messages.Add(new MessagesMessages
{
Asunto = "Hola"
});
Messages.Add(new MessagesMessages
{
Asunto = "Hola1"
});
Messages.Add(new MessagesMessages
{
Asunto = "Hola2"
});
Messages.Add(new MessagesMessages
{
Asunto = "Hola3"
});
}
}
LoginPage.xaml.cs
public partial class LoginPage : ContentPage
{
public LoginPage(LoginPageViewModel loginPageViewModel)
{
InitializeComponent();
BindingContext= loginPageViewModel;
}
}
MauiProgram.cs
builder.Services.AddTransient<LoginPage>();
builder.Services.AddSingleton<ViewModels.LoginPageViewModel>();

you are not following the naming convention
The name of the generated command will be created based on the method
name. The generator will use the method name and append "Command" at
the end, and it will strip the "On" prefix, if present. Additionally,
for asynchronous methods, the "Async" suffix is also stripped before
"Command" is appeneded.
to generate a command named MessageSelectedCommand your method should look like
[RelayCommand]
async void MessageSelected()

Related

NET MAUI CommunityToolkit.Mvvm not validating

I have a view model:
public delegate void NotifyWithValidationMessages(Dictionary<string, string?> validationDictionary);
public partial class BaseViewModel : ObservableValidator
{
public event NotifyWithValidationMessages? ValidationCompleted;
public virtual ICommand ValidateCommand => new RelayCommand(() => ValidateModel());
private ValidationContext validationContext;
public BaseViewModel()
{
validationContext = new ValidationContext(this);
}
[IndexerName("ErrorDictionary")]
public ValidationStatus this[string propertyName]
{
get
{
ClearErrors();
ValidateAllProperties();
var errors = this.GetErrors()
.ToDictionary(k => k.MemberNames.First(), v => v.ErrorMessage) ?? new Dictionary<string, string?>();
var hasErrors = errors.TryGetValue(propertyName, out var error);
return new ValidationStatus(hasErrors, error ?? string.Empty);
}
}
private void ValidateModel()
{
ClearErrors();
ValidateAllProperties();
var validationMessages = this.GetErrors()
.ToDictionary(k => k.MemberNames.First().ToLower(), v => v.ErrorMessage);
ValidationCompleted?.Invoke(validationMessages);
}
}
public partial class LoginModel : BaseViewModel
{
protected string email;
protected string password;
[Required]
[DataType(DataType.EmailAddress)]
public string Email
{
get => this.email;
set
{
SetProperty(ref this.email, value, true);
ClearErrors();
ValidateAllProperties();
OnPropertyChanged("ErrorDictionary[Email]");
}
}
[Required]
[DataType(DataType.Password)]
public string Password
{
get => this.password;
set
{
SetProperty(ref this.password, value, true);
ClearErrors();
ValidateAllProperties();
OnPropertyChanged("ErrorDictionary[Password]");
}
}
}
public partial class LoginViewModel : LoginModel
{
private readonly ISecurityClient securityClient;
public LoginViewModel(ISecurityClient securityClient) : base()
{
this.securityClient = securityClient;
}
public ICommand LoginCommand => new RelayCommand(async() => await LoginAsync());
public ICommand NavigateToRegisterPageCommand => new RelayCommand(async () => await Shell.Current.GoToAsync(PageRoutes.RegisterPage, true));
private async Task LoginAsync()
{
if (this?.HasErrors ?? true)
return;
var requestParam = this.ConvertTo<LoginModel>();
var response = await securityClient.LoginAsync(requestParam);
if (response is null)
{
await Application.Current.MainPage.DisplayAlert("", "Login faild, or unauthorized", "OK");
StorageService.Secure.Remove(StorageKeys.Secure.JWT);
return;
}
await StorageService.Secure.SaveAsync<JWTokenModel>(StorageKeys.Secure.JWT, response);
await Shell.Current.GoToAsync(PageRoutes.HomePage, true);
}
}
The view looks like this:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:local="clr-namespace:Backend.Models;assembly=Backend.Models"
xmlns:vm="clr-namespace:MauiUI.ViewModels"
x:Class="MauiUI.Pages.LoginPage"
x:DataType="vm:LoginViewModel"
Shell.NavBarIsVisible="False">
<ScrollView>
<VerticalStackLayout Spacing="25" Padding="20,0"
VerticalOptions="Center">
<VerticalStackLayout>
<Label Text="Welcome to Amazons of Vollyeball" FontSize="28" TextColor="Gray" HorizontalTextAlignment="Center" />
</VerticalStackLayout>
<Image Source="volleyball.png"
HeightRequest="250"
WidthRequest="250"
HorizontalOptions="Center" />
<StackLayout Orientation="Horizontal">
<Frame ZIndex="1" HasShadow="True" BorderColor="White"
HeightRequest="55" WidthRequest="55" CornerRadius="25"
Margin="0,0,-32,0">
<Image Source="email.png" HeightRequest="30" WidthRequest="30" />
</Frame>
<Frame HasShadow="True" Padding="0" BorderColor="White" HeightRequest="55" HorizontalOptions="FillAndExpand">
<Entry x:Name="email" Margin="35,0,20,0" VerticalOptions="Center" Placeholder="email" Keyboard="Email"
Text="{Binding Email, Mode=TwoWay}"
toolkit:SetFocusOnEntryCompletedBehavior.NextElement="{x:Reference password}"
ReturnType="Next">
<Entry.Behaviors>
<toolkit:EventToCommandBehavior
EventName="TextChanged"
Command="{Binding ValidateCommand}" />
</Entry.Behaviors>
</Entry>
</Frame>
</StackLayout>
<Label x:Name="lblValidationErrorEmail" Text="{Binding [Email].Error}" TextColor="Red" />
<StackLayout Orientation="Horizontal">
<Frame ZIndex="1" HasShadow="True" BorderColor="White"
HeightRequest="55" WidthRequest="55" CornerRadius="25"
Margin="0,0,-32,0">
<Image Source="password.jpg" HeightRequest="30" WidthRequest="30"/>
</Frame>
<Frame HasShadow="True" Padding="0" BorderColor="White" HeightRequest="55" HorizontalOptions="FillAndExpand">
<Entry x:Name="password" Margin="35,0,20,0" VerticalOptions="Center" Placeholder="password" IsPassword="True"
Text="{Binding Password, Mode=TwoWay}">
<Entry.Behaviors>
<toolkit:EventToCommandBehavior
EventName="TextChanged"
Command="{Binding ValidateCommand}" />
</Entry.Behaviors>
</Entry>
</Frame>
</StackLayout>
<Label x:Name="lblValidationErrorPassword" Text="{Binding [Password].Error}" TextColor="Red" />
<Button Text="Login" WidthRequest="120" CornerRadius="25" HorizontalOptions="Center" BackgroundColor="Blue"
Command="{Binding LoginCommand}" />
<StackLayout Orientation="Horizontal" Spacing="5" HorizontalOptions="Center">
<Label Text="Don't have an account?" TextColor="Gray"/>
<Label>
<Label.FormattedText>
<FormattedString>
<Span Text="Register" TextColor="Blue">
<Span.GestureRecognizers>
<TapGestureRecognizer Command="{Binding NavigateToRegisterPageCommand}" />
</Span.GestureRecognizers>
</Span>
</FormattedString>
</Label.FormattedText>
</Label>
</StackLayout>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
public partial class LoginPage : ContentPage
{
private RegisterViewModel viewModel => BindingContext as RegisterViewModel;
public LoginPage(LoginViewModel viewModel)
{
InitializeComponent();
viewModel.ValidationCompleted += OnValidationHandler;
BindingContext = viewModel;
#if ANDROID
MauiUI.Platforms.Android.KeyboardHelper.HideKeyboard();
#elif IOS
MauiUI.Platforms.iOS.KeyboardHelper.HideKeyboard();
#endif
}
private void OnValidationHandler(Dictionary<string, string> validationMessages)
{
if (validationMessages is null)
return;
lblValidationErrorEmail.Text = validationMessages.GetValueOrDefault("email");
lblValidationErrorPassword.Text = validationMessages.GetValueOrDefault("password");
}
}
When the
public ValidationStatus this[string propertyName] or the ValidateModel() triggers in BaseViewModel, the this.GetErrors() form the ObservableValidator class, return no errors, even if there are validation errors.
Interesting part was that, when I did not use MVVM aproach, and used LoginModel that inherited the BaseViewModel, then worked.
I am out of idea.
thnx
I do not write my own properties. Instead, I let MVVM handle it.
Lets say we have this:
public partial class MainViewModel : BaseViewModel
{
[Required(ErrorMessage = "Text is Required Field!")]
[MinLength(5, ErrorMessage = "Text length is minimum 5!")]
[MaxLength(10, ErrorMessage = "Text length is maximum 10!")]
[ObservableProperty]
string _text = "Hello";
Where BaseViewModel is inheriting ObservableValidator.
Now I can use Validation command:
[RelayCommand]
void Validate()
{
ValidateAllProperties();
if (HasErrors)
Error = string.Join(Environment.NewLine, GetErrors().Select(e => e.ErrorMessage));
else
Error = String.Empty;
IsTextValid = (GetErrors().ToDictionary(k => k.MemberNames.First(), v => v.ErrorMessage) ?? new Dictionary<string, string?>()).TryGetValue(nameof(Text), out var error);
}
Or use partial method:
partial void OnTextChanged(String text)
{
ValidateAllProperties();
if (HasErrors)
Error = string.Join(Environment.NewLine, GetErrors().Select(e => e.ErrorMessage));
else
Error = String.Empty;
IsTextValid = (GetErrors().ToDictionary(k => k.MemberNames.First(), v => v.ErrorMessage) ?? new Dictionary<string, string?>()).TryGetValue(nameof(Text), out var error);
}
Where Error is:
[ObservableProperty]
string _error;
And IsTextValid is:
[ObservableProperty]
bool _isTextValid;
Now you can bind those properties to whatever you want to display the error, or indicate that there is an error with your Text.
This is a working example, using validation, CommunityToolkit.MVVM and BaseViewModel class.
I made a demo based on your code and make some debugs. The thing i found is that you just clear the Errors ClearErrors();. Then you could not get any message.
Workaround:
In LoginModel the Email setter, put ClearErrors(); before SetProperty(ref this.email, value, true);.
[DataType(DataType.EmailAddress)]
public string Email
{
get => this.email;
set
{
ClearErrors();
SetProperty(ref this.email, value, true);
....
}
}
In BaseViewModel, comment out other ClearErrors(); in Indexer and ValidateModel() since you have already cleared it.
[IndexerName("ErrorDictionary")]
public ValidationStatus this[string propertyName]
{
get
{
//ClearErrors();
ValidateAllProperties();
...
}
}
private void ValidateModel()
{
//ClearErrors();
ValidateAllProperties();
....
}
However, your code show two ways of invoking the ValidateAllProperty() :
First is because in the .xaml, you set Text="{Binding Email, Mode=TwoWay}". That means when changing the text, the setter of Email in your LoginModel will fire and so raise propertyChanged for the Indexer.
Second is EventToCommandBehavior set in the .xaml. This also invoke
ValidateAllProperty(). And pass the text to label Text which has already binded Text="{Binding [Email].Error}".
From my point of view, one is enough. You have to decide which one to use. Better not mix these two methods together that may cause troubles.
Also for MVVM structure, you could refer to Model-View-ViewModel (MVVM).
Hope it works for you.

.net MAUI Picker SelectedIndex does not cause item to display

I cannot get the expected behaviour of SelectedIndex to work. The Item is not shown. The ItemSource, ItemDisplayBinding and SelectedItem are working when the picker is selected, But when the view is first displayed the Pickers are not showing the objects from the List that they are bound to.
I have created a test .Maui APP as follows;
The View MainPage.xml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:models="clr-namespace:MauiPicker;assembly=MauiPicker"
xmlns:viewModels="clr-namespace:MauiPicker"
x:Class="MauiPicker.MainPage"
x:DataType="viewModels:MainViewModel">
<Grid
ColumnDefinitions="*"
RowDefinitions="*,*">
<CollectionView
Grid.Row="0"
Grid.Column="0"
ItemsSource="{Binding PartAResultLists}"
SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:PartAResultList">
<Grid Padding="5">
<Border>
<Grid Padding="10"
ColumnDefinitions="Auto,*"
RowDefinitions="Auto"
RowSpacing="7">
<Label Text="Outlet Type:"
Grid.Column="0" Grid.Row="0"
HorizontalOptions="End"
VerticalOptions="Center"
Margin="0,0,0,0"/>
<Border
Grid.Column="1"
Grid.Row="0"
Grid.ColumnSpan="2">
<Picker
Title="Select an Outlet"
ItemsSource="{Binding Source={RelativeSource AncestorType={x:Type viewModels:MainViewModel}}, Path=Outlets}"
ItemDisplayBinding="{Binding Name}"
SelectedIndex="{Binding OutletIndex}"
SelectedItem="{Binding OutletName}">
</Picker>
</Border>
</Grid>
</Border>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Button
Grid.Row="1"
Grid.Column="0"
Text="Reload List"
HorizontalOptions="Center"
VerticalOptions="Center"
Command="{Binding Source={RelativeSource AncestorType={x:Type viewModels:MainViewModel}}, Path=LoadResultsCommand}">
</Button>
</Grid>
</ContentPage>
The code behind MainPage.xml.cs
namespace MauiPicker;
public partial class MainPage : ContentPage
{
public MainPage(MainViewModel vm)
{
InitializeComponent();
BindingContext = vm;
}
}
The ViewModel MainViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MvvmHelpers;
namespace MauiPicker
{
public partial class MainViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject
{
public MainViewModel()
{
LoadResults();
}
[RelayCommand]
async Task LoadResults()
{
Outlets = new ObservableRangeCollection<Outlet>
{
new Outlet(){Name="Outlet0"},
new Outlet(){Name="Outlet1"},
new Outlet(){Name="Outlet2"},
};
PartAResultLists = new ObservableRangeCollection<PartAResultList>
{
new PartAResultList(){OutletIndex = 0, OutletName= new Outlet(){Name="Outlet0" } },
new PartAResultList(){OutletIndex=1, OutletName= new Outlet(){Name="Outlet1" }},
new PartAResultList(){OutletIndex = 2, OutletName= new Outlet(){Name="Outlet2" }},
new PartAResultList(){OutletIndex = 0, OutletName= new Outlet(){Name="Outlet0" }},
new PartAResultList(){OutletIndex = 2, OutletName= new Outlet(){Name="Outlet2" }}
};
}
[ObservableProperty]
ObservableRangeCollection<Outlet> outlets;
[ObservableProperty]
ObservableRangeCollection<PartAResultList> partAResultLists;
}
}
The models;
using CommunityToolkit.Mvvm.ComponentModel;
namespace MauiPicker
{
public partial class Outlet : ObservableObject
{
[ObservableProperty]
public string name;
}
}
using CommunityToolkit.Mvvm.ComponentModel;
namespace MauiPicker
{
public partial class PartAResultList : ObservableObject
{
[ObservableProperty]
public Outlet outletName;
[ObservableProperty]
public int outletIndex;
}
}
MauiProgram.cs
using CommunityToolkit.Maui;
using Microsoft.Extensions.Logging;
namespace MauiPicker;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkit()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
builder.Services.AddSingleton<MainPage>();
builder.Services.AddSingleton<MainViewModel>();
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}
First I want to point out, for anyone who is reading this, that the code in the question is bound correctly.
Second, I want to say that new Outlet(Name="1") and new Outlet(Name="1") are not equal. Just because some property has the same value, does not mean that the objects will be the same. They are saved at two different places, and they are not the same. Not unless you make sure that only "Name" matters, when checking if they are equal.
Third, If you check the community toolkit MVVM autogenerated property code, you will see something like:
if(!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(name, value))
And calling OnChanged in that IF block. This is optimization to help only waste resources, when something has to be re-rendered.
This is why, when you set:
new PartAResultList(){OutletIndex = 0...
And then if you call, for example:
PartAResultLists[0].OutletIndex = 0;
The picker will remain blank. But if you call this:
PartAResultLists[0].OutletIndex = 1;
Surprisingly it will change.
(Again, giving example with the index, because the object will not work for the reasons I pointed out earlier).
There is no need to bind to Object and to Index at the same time. In your situation this can only lead to more bugs.
Your bindings are setup well. You have to fix your code around them here and there.
Edit: You do not have to actually change the value. Just trying to explain the problem that OnPropertyChanged("OutletIndex") is not called.

Xamarin Layout can't receive focus

I'm trying to create a compound view component in Xamarin Forms called FormElement which is composed of two labels and an Entry:
<?xml version="1.0" encoding="UTF-8"?>
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:custom="clr-namespace:Mynamespace;assembly=Mynamespace"
x:Class="Mynamespace.Components.FormEntry">
<StackLayout Orientation="Horizontal">
<Label x:Name="formRequiredStar"
IsVisible="{Binding IsRequired}"
Text="*" TextColor="Red"
FontSize="15"
FontAttributes="Bold"
Margin="-12,0,0,0"
HorizontalOptions="Start" />
<Label x:Name="formLabel"
HorizontalOptions="Start"
Text="{Binding LabelText}"
TextColor="{Binding LabelTextColor}"
FontSize="{Binding LabelTextFontSize}"
FontAttributes="{Binding LabelTextFontStyle}" />
</StackLayout>
<Frame BorderColor="Black"
CornerRadius="7"
Padding="5,0"
Margin="0,-3,0,0"
HasShadow="false">
<Entry x:Name="mainEntry"
Keyboard="{Binding KeybdType}"
Placeholder="{Binding EntryPlaceHolder}"
TextColor="Black"
FontSize="Default"
HeightRequest="{Binding EntryHeight}" />
</Frame>
</StackLayout>
Next, I want to switch focus from the Entry to a "next" element when the user taps the DONE button, so I do:
namespace Mynamespace.Components
{
public partial class FormEntry : StackLayout
{
public VisualElement NextFocus
{
get { return (VisualElement)GetValue(NextFocusProperty); }
set { SetValue(NextFocusProperty, value); }
}
public static readonly BindableProperty NextFocusProperty =
BindableProperty.Create(nameof(NextFocus),
typeof(VisualElement),
typeof(FormEntry),
null,
Xamarin.Forms.BindingMode.OneWay);
public FormEntry()
{
InitializeComponent();
BindingContext = this;
mainEntry.Completed += (s, e) =>
{
if (NextFocus != null)
{
NextFocus.Focus();
}
};
}
}
}
Next, in order for a FormEntry to be the target of NextFocus, I tried adding
this.Focused += (s,e) => { mainEntry.Focus(); };
to the constructor, but the handler is never called, and I also tried overriding
public new void Focus() {
mainEntry.Focus();
}
but this method is never called. Layout classes are descended from VisualElement so they should inherit Focused. Is there something about Layout objects that I'm missing? I could understand that Layout objects aren't usually the target of focus, but the event handler is supposedly there so I ought to be able to use it.
Here's an example of how I utilize the FormEntry on a login screen:
<!-- Email -->
<controls:FormEntry x:Name="usernameEntry"
Margin="25,40,25,0"
IsRequired="true"
EntryHeight="40"
KeybdType="Email"
NextFocus="{x:Reference passwordEntry}"
LabelText="{il8n:Translate Emailorusername}"
EntryPlaceHolder="{il8n:Translate EnterUsername}">
</controls:FormEntry>
<!-- Password -->
<controls:FormEntry x:Name="passwordEntry"
Margin="25,0,25,0"
IsRequired="true"
EntryHeight="40"
LabelText="{il8n:Translate Password}"
EntryPlaceHolder="{il8n:Translate EnterPassword}" />
I think you have get the nextfocus element, you can get mainEntry from nextfocus, like this:
public FormEntry ()
{
InitializeComponent ();
BindingContext = this;
mainEntry.Completed += (s, e) =>
{
if (NextFocus != null)
{
FormEntry formentry = (FormEntry)NextFocus;
Entry entry = formentry.mainEntry;
entry.Focus();
}
};
}
Then you can find you will get focus.

How can I set DataTemplate to a ContentControl in a PageResource in Windows Universal App 10?

How can I set DataTemplate to a ContentControl in a PageResource? I would like to display a UserControl in my ContentControl and I want use the ContentControl like a navigation region. So it can change the UserControls what are displayed in it.
I have a Shell.xaml:
<Page
x:Class="MyProject.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyProject"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:View="using:MyProject.View"
xmlns:ViewModel="using:MyProject.ViewModel">
<Page.DataContext>
<ViewModel:ShellViewModel />
</Page.DataContext>
<Page.Resources>
<DataTemplate>
<View:MyUserControlViewModel1 />
</DataTemplate>
<DataTemplate>
<View:MyUserControlViewModel2 />
</DataTemplate>
</Page.Resources>
<StackPanel>
<ItemsControl ItemsSource="{Binding PageViewModels}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Name}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ContentControl Content="{Binding CurrentPageViewModel}">
</ContentControl>
</StackPanel>
</Page>
My Shell's view model is:
namespace MyProject.ShellViewModel
{
class ShellViewModel : ObservableObject
{
#region Fields
private ICommand _changePageCommand;
private IPageViewModel _currentPageViewModel;
private List<IPageViewModel> _pageViewModels;
#endregion
#region Properties / Commands
public List<IPageViewModel> PageViewModels
{
get
{
if (_pageViewModels == null)
{
_pageViewModels = new List<IPageViewModel>();
}
return _pageViewModels;
}
}
public IPageViewModel CurrentPageViewModel
{
get { return _currentPageViewModel; }
set
{
if (_currentPageViewModel != value)
{
_currentPageViewModel = value;
OnPropertyChanged("CurrentPageViewModel");
}
}
}
#endregion
#region Methods
public ShellViewModel()
{
PageViewModels.Add(new MyUserControlViewModel1());
PageViewModels.Add(new MyUserControlViewModel2());
CurrentPageViewModel = PageViewModels[0];
}
#endregion
}
}
I tried set Page.Resource like below from this link: Window vs Page vs UserControl for WPF navigation?
<Window.Resources>
<DataTemplate DataType="{x:Type local:HomeViewModel}">
<local:HomeView /> <!-- This is a UserControl -->
</DataTemplate>
<DataTemplate DataType="{x:Type local:ProductsViewModel}">
<local:ProductsView /> <!-- This is a UserControl -->
</DataTemplate>
</Window.Resources>
But these use another namespaces and it doesn't work for me because my app is a Windows 10 Universal App and there is no DataType attribute for the DataTemplate for example.
I am trying to make my application using MVVM pattern (if it was not obiously from the code snippets).
You can do it by creating a class derived from DataTemplateSelector.
In this class you can override SelectTemplateCore method, that will return the DataTemplate you need based on the data type. To make all this more auto-magical, you can set the key of each template to match the name of the class and then search for the resource with that name retrieved using GetType().Name.
To be able to provide specific implementations on different levels, you can walk up the tree using VisualTreeHelper.GetParent() until the matching resource is found and use Application.Current.Resources[ typeName ] as fallback.
To use your custom template selector, just set it to ContentTemplateSelector property of the ContentControl.
Example
Here is the sample implementation of an AutoDataTemplateSelector
public class AutoDataTemplateSelector : DataTemplateSelector
{
protected override DataTemplate SelectTemplateCore( object item ) => GetTemplateForItem( item, null );
protected override DataTemplate SelectTemplateCore( object item, DependencyObject container ) => GetTemplateForItem( item, container );
private DataTemplate GetTemplateForItem( object item, DependencyObject container )
{
if ( item != null )
{
var viewModelTypeName = item.GetType().Name;
var dataTemplateInTree = FindResourceKeyUpTree( viewModelTypeName, container );
//return or default to Application resource
return dataTemplateInTree ?? ( DataTemplate )Application.Current.Resources[ viewModelTypeName ];
}
return null;
}
/// <summary>
/// Tries to find the resources up the tree
/// </summary>
/// <param name="resourceKey">Key to find</param>
/// <param name="container">Current container</param>
/// <returns></returns>
private DataTemplate FindResourceKeyUpTree( string resourceKey, DependencyObject container )
{
var frameworkElement = container as FrameworkElement;
if ( frameworkElement != null )
{
if ( frameworkElement.Resources.ContainsKey( resourceKey ) )
{
return frameworkElement.Resources[ resourceKey ] as DataTemplate;
}
else
{
return FindResourceKeyUpTree( resourceKey, VisualTreeHelper.GetParent( frameworkElement ) );
}
}
return null;
}
}
You can now instantiate it as a resource and create resources for each type of ViewModel you use
<Application.Resources>
<local:AutoDataTemplateSelector x:Key="AutoDataTemplateSelector" />
<!-- sample viewmodel data templates -->
<DataTemplate x:Key="RedViewModel">
<Rectangle Width="100" Height="100" Fill="Red" />
</DataTemplate>
<DataTemplate x:Key="BlueViewModel">
<Rectangle Width="100" Height="100" Fill="Blue" />
</DataTemplate>
</Application.Resources>
And now you can use it with the ContentControl as follows:
<ContentControl ContentTemplateSelector="{StaticResource AutoDataTemplateSelector}"
Content="{x:Bind CurrentViewModel, Mode=OneWay}" />
I have put the sample solution on GitHub

Why does DelegateCommand not work the same for Button and MenuItem?

This code shows that the Delegate Command from the Visual Studio MVVM template works differently when used with MenuItem and Button:
with Button, the command method has access to changed OnPropertyChanged values on the ViewModel
when using MenuItem, however, the command method does not have access to changed OnPropertyChanged values
Does anyone know why this is the case?
MainView.xaml:
<Window x:Class="TestCommand82828.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:TestCommand82828.Commands"
Title="Main Window" Height="400" Width="800">
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Command="{Binding DoSomethingCommand}" Header="Do Something" />
</MenuItem>
</Menu>
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Top">
<Button Command="{Binding DoSomethingCommand}" Content="test"/>
<TextBlock Text="{Binding Output}"/>
<TextBox Text="{Binding TheInput}"/>
</StackPanel>
</DockPanel>
</Window>
MainViewModel.cs:
using System;
using System.Windows;
using System.Windows.Input;
using TestCommand82828.Commands;
namespace TestCommand82828.ViewModels
{
public class MainViewModel : ViewModelBase
{
#region ViewModelProperty: TheInput
private string _theInput;
public string TheInput
{
get
{
return _theInput;
}
set
{
_theInput = value;
OnPropertyChanged("TheInput");
}
}
#endregion
#region DelegateCommand: DoSomething
private DelegateCommand doSomethingCommand;
public ICommand DoSomethingCommand
{
get
{
if (doSomethingCommand == null)
{
doSomethingCommand = new DelegateCommand(DoSomething, CanDoSomething);
}
return doSomethingCommand;
}
}
private void DoSomething()
{
Output = "did something, the input was: " + _theInput;
}
private bool CanDoSomething()
{
return true;
}
#endregion
#region ViewModelProperty: Output
private string _output;
public string Output
{
get
{
return _output;
}
set
{
_output = value;
OnPropertyChanged("Output");
}
}
#endregion
}
}
I think what you're seeing is because MenuItems don't take focus away from a TextBox, and by default, a TextBox only pushes changes back to its bound source when focus shifts away from it. So when you click the button, the focus shifts to the button, writing the TextBox's value back to _theInput. When you click the MenuItem, however, focus remains in the TextBox so the value is not written.
Try changing your TextBox declaration to:
<TextBox Text="{Binding TheInput,UpdateSourceTrigger=PropertyChanged}"/>
Or alternatively, try switching to DelegateCommand<t>, which can receive a parameter, and pass the TextBox's text to it:
<MenuItem Command="{Binding DoSomethingCommand}"
CommandParameter="{Binding Text,ElementName=inputTextBox}" />
...
<TextBox x:Name="inputTextBox" Text="{Binding TheInput}" />