How to pass commands across different XAML files/controls - c#

I come from a WPF background so I thought I'd experiment with building a to-do app in WinUI 3. The app structure is a little overdesigned as I'm trying build it out like a more complex app. For that reason I have a ToDoTaskView and ToDoTaskViewModel, along with a MainWindowView and MainWindowViewModel even though it'd be much easier to build the entire app in a single XAML file.
The ToDoTaskView has a delete button, but the delete command lives on the MainWindowViewModel, as that's where the list that it must be deleted from lives. I think this a pretty common pattern where a sub-view needs to send a command to a parent view model.
The (abridged) MainWindowView:
<UserControl>
<ItemsRepeater ItemsSource="{Binding Tasks}">
<DataTemplate>
<local:ToDoTaskView />
</DataTemplate>
</ItemsRepeater>
</UserControl>
And the (heavily abridged) ToDoTaskView:
<UserControl>
<Button Command="???">Delete</Button>
</UserControl>
In WPF there's many ways to deal with this.
RoutedCommand
My prefered method. The MainWindowView can listen for a custom ToDoTaskDeleted routed command and bind to the command on the view model. Then any UI element anywhere underneath MainWindowView can fire said event and rest easy knowing it'll be handled somewhere above it on the visual tree.
There's no RoutedCommand in WinUI 3, and even worse, routed events are locked down and you can't define custom ones. So even building a custom RoutedCommand implementation would be difficult.
DynamicResource
I can define a StandardUICommand in MainWindowView.Resources, bind it to the command in the view model, then in ToDoTaskView I can use {DynamicResource DeleteCommand} to have the resource system search up the visual tree for the command.
Except I can't. WinUI3 doesn't have DynamicResource, only StaticResource. And since the two views are in different XAML files, and ToDoTaskView in a templated context, StaticResource can't resolve the resource name between them.
I think this could work for resources in App.xaml, but I'd rather not shove every command into the top level scope instead of keeping them where they belong.
All the commanding examples in the Microsoft docs seem to assume that the button and handler are in the same file, or they directly pass a reference to the command through to the child view's DataContext.
RelativeAncestor
Peter below reminded me that I tried this too, and found it's missing in WinUI 3. RelativeSource doesn't support any kind of ancestor discovery.
Manual Kludge
Setting up a direct reference from ToDoTaskViewModel to MainWindowViewModel is certainly possible, but I hate it. After all, who's to guarantee that this particular to do item is part of a list at any one moment? Maybe it lives in a pop-up dialog as a reminder? Handling this kind of thing through the visual tree is the Correct(tm) way to do it.
I wouldn't accept a PR from a coworker on my WPF project with this solution. But I can't seem to find any better way in WinUI 3.
Have I missed something about WinUI 3? Is it just not mature enough yet to have a solution? It seems like this scenario isn't so uncommon that it would be completely unsupported.

In this case, I'd create an ICommand dependency property, DeleteCommand and and bind a command in the view model. Here's a sample code using the CommunityToolkit.Mvvm NuGet package.
MainWindow.xaml
The MainWindow is named, "ThisWindow" in this case, so we can access its ViewModel from the ItemTemplate.
The DeleteCommandParameter is bound to the DataContext of the item, ToDoTaskViewModel in this case.
<Window
x:Class="ToDoApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:ToDoApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="ThisWindow"
mc:Ignorable="d">
<Grid RowDefinitions="Auto,*">
<StackPanel
Grid.Row="0"
Orientation="Horizontal">
<TextBox x:Name="NewToDo" />
<Button
Command="{x:Bind ViewModel.AddToDoCommand}"
CommandParameter="{x:Bind NewToDo.Text, Mode=OneWay}"
Content="Add" />
</StackPanel>
<ScrollViewer Grid.Row="1">
<ItemsRepeater ItemsSource="{x:Bind ViewModel.ToDoTasks, Mode=OneWay}">
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="local:ToDoTaskViewModel">
<local:ToDoTaskView
DeleteCommand="{Binding ElementName=ThisWindow, Path=ViewModel.DeleteToDoCommand}"
DeleteCommandParameter="{x:Bind}"
ToDo="{x:Bind ToDo, Mode=OneWay}" />
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</Grid>
</Window>
MainWindow.xaml.cs
using Microsoft.UI.Xaml;
namespace ToDoApp;
public sealed partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
public MainWindowViewModel ViewModel { get; } = new();
}
MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
namespace ToDoApp;
[ObservableObject]
public partial class MainWindowViewModel
{
[ObservableProperty]
private ObservableCollection<ToDoTaskViewModel> toDoTasks = new();
[RelayCommand]
private void AddToDo(string todo)
{
ToDoTasks.Add(new ToDoTaskViewModel() { ToDo = todo });
}
[RelayCommand]
private void DeleteToDo(ToDoTaskViewModel toDoTask)
{
ToDoTasks.Remove(toDoTask);
}
}
ToDoTaskView.xaml
<UserControl
x:Class="ToDoApp.ToDoTaskView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:ToDoApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid ColumnDefinitions="*,Auto">
<TextBlock
Grid.Column="0"
Text="{x:Bind ToDo, Mode=OneWay}" />
<Button
Grid.Column="1"
Command="{x:Bind DeleteCommand, Mode=OneWay}"
CommandParameter="{x:Bind DeleteCommandParameter, Mode=OneWay}"
Content="Delete" />
</Grid>
</UserControl>
ToDoTaskView.xaml.cs
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Windows.Input;
namespace ToDoApp;
public sealed partial class ToDoTaskView : UserControl
{
public static readonly DependencyProperty ToDoProperty = DependencyProperty.Register(
nameof(ToDo),
typeof(string),
typeof(ToDoTaskView),
new PropertyMetadata(default));
public static readonly DependencyProperty DeleteCommandProperty = DependencyProperty.Register(
nameof(DeleteCommand),
typeof(ICommand),
typeof(ToDoTaskView),
new PropertyMetadata(default));
public static readonly DependencyProperty DeleteCommandParameterProperty = DependencyProperty.Register(
nameof(DeleteCommandParameter),
typeof(object),
typeof(ToDoTaskView),
new PropertyMetadata(default));
public ToDoTaskView()
{
this.InitializeComponent();
}
public string ToDo
{
get => (string)GetValue(ToDoProperty);
set => SetValue(ToDoProperty, value);
}
public ICommand DeleteCommand
{
get => (ICommand)GetValue(DeleteCommandProperty);
set => SetValue(DeleteCommandProperty, value);
}
public object DeleteCommandParameter
{
get => (object)GetValue(DeleteCommandParameterProperty);
set => SetValue(DeleteCommandParameterProperty, value);
}
}
ToDoTaskViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
namespace ToDoApp;
[ObservableObject]
public partial class ToDoTaskViewModel
{
[ObservableProperty]
private string toDo = string.Empty;
}

Ok I have a solution. I cannot emphasize enough how much of a disgusting hack this is. Normally I'd be embarrassed to post this, but the only ones who should be embarrassed are Microsoft for publishing Win UI 3 in its current state and claiming it's capable of making real applications.
The gist of this is to mimic Ancestor-type RelativeSource binding in WPF. We create two attached properties - ParentContextViewType to specify the type of the ancestor we're looking for - and ParentContextView which is automatically assigned a reference to the desired parent view instance when the child loads. (I'd have made ParentContextView a readonly property, but of course, Win UI doesn't support that...) Then for the child button, we do a RelativeSource Self binding to the attached ParentContextView property, then adding the rest of the path, just like we would with a legit ancestor type bind.
Here goes (and may god have mercy on my soul):
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
namespace ParentBinding
{
public static class Hacks
{
public static DependencyProperty ParentContextView =
DependencyProperty.RegisterAttached(
"ParentContextView",
typeof(FrameworkElement),
typeof(Hacks),
new PropertyMetadata(null));
public static FrameworkElement GetParentContextView(DependencyObject d)
{
return d.GetValue(ParentContextView) as FrameworkElement;
}
public static void SetParentContextView(DependencyObject d, FrameworkElement view)
{
d.SetValue(ParentContextView, view);
}
public static DependencyProperty ParentContextViewTypeProperty =
DependencyProperty.RegisterAttached(
"ParentContextViewType",
typeof(Type),
typeof(Hacks),
new PropertyMetadata(null, (d, e) =>
{
if (!(d is FrameworkElement fe))
return;
if (e.OldValue != null)
fe.Loaded -= OnParentContextFeLoaded;
if (e.NewValue != null)
fe.Loaded += OnParentContextFeLoaded;
}));
private static void OnParentContextFeLoaded(object sender, RoutedEventArgs e)
{
if (!(sender is FrameworkElement fe))
return;
var type = GetParentContextViewType(fe);
if (type == null)
return;
while (!type.IsAssignableFrom(fe.GetType()) &&
(fe = VisualTreeHelper.GetParent(fe) as FrameworkElement) != null)
{
}
SetParentContextView(sender as DependencyObject, fe);
}
public static Type GetParentContextViewType(DependencyObject d)
{
return d.GetValue(ParentContextViewTypeProperty) as Type;
}
public static void SetParentContextViewType(DependencyObject d, Type val)
{
d.SetValue(ParentContextViewTypeProperty, val);
}
}
}
A use-case:
Model stuff:
using Microsoft.UI.Xaml.Input;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace ParentBinding
{
public class Command : ICommand
{
Action<object> _action;
public Command(Action<object> action)
{
_action = action;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter) => true;
public void Execute(object? parameter)
{
_action?.Invoke(parameter);
}
}
public class Parent
{
public ObservableCollection<Child> Children { get; set; }
private Command _deleteChildCommand;
public ICommand DeleteChildCommand =>
_deleteChildCommand ?? (_deleteChildCommand = new Command((p) =>
{
if (!(p is Child ch))
return;
this.Children.Remove(ch);
}));
}
public class Child
{
public string Name { get; set; }
public override string ToString() => this.Name;
}
}
Main Window:
<Window x:Class="ParentBinding.MainWindow"
x:Name="_main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ParentBinding"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<ListView DataContext="{Binding ElementName=_main, Path=Parent}"
ItemsSource="{Binding Children}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Child">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" />
<Button local:Hacks.ParentContextViewType="ListView"
Grid.Column="1"
CommandParameter="{Binding}"
Content="Delete"
Command="{Binding
Path=(local:Hacks.ParentContextView).DataContext.DeleteChildCommand,
RelativeSource={RelativeSource Self}}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Window>
using Microsoft.UI.Xaml;
namespace ParentBinding
{
public sealed partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
public Parent Parent { get; } = new Parent
{
Children = new System.Collections.ObjectModel.ObservableCollection<Child>
{
new Child
{
Name = "Larry"
},
new Child
{
Name = "Curly"
},
new Child
{
Name = "Moe"
}
}
};
}
}
Amazingly, it works, and one of the reasons I was so curious to try it and post it is that it is, more or less, a general purpose substitute for ancestor type binding in WinUI 3. Hope someone finds it useful.

Related

How do I go to another view from a view in MVVM WPF?

Here I have a WPF application that is made with the MVVM structure. I am fairly new to C# WPF and am not familiar with this concept. I am attempting to switch to another view through a function in one view via the press of a button.
Here is what the application looks like,
Once the Login button is pressed a function is triggered that will validate the inputs and if valid switch to another view. Which would look like such,
File Structure
How can i switch the views ?
Below are some code for reference.
MainWindow.xaml
<Window x:Class="QuizAppV2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:QuizAppV2"
xmlns:viewModel="clr-namespace:QuizAppV2.MVVM.ViewModel"
mc:Ignorable="d"
Height="600" Width="920"
WindowStartupLocation="CenterScreen"
WindowStyle="None"
ResizeMode="NoResize"
Background="Transparent"
AllowsTransparency="True">
<Window.DataContext>
<viewModel:MainViewModel/>
</Window.DataContext>
<Border Background="#272537"
CornerRadius="20">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="75"/>
<RowDefinition/>
<RowDefinition Height="25"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Online Quiz"
Grid.Column="1"
FontSize="20"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<StackPanel Grid.Column="2"
Margin="30,20"
Orientation="Horizontal"
HorizontalAlignment="Right"
VerticalAlignment="Top">
<Button Content="–"
Background="#00CA4E"
Style="{StaticResource UserControls}"
Click="Minimise"/>
<Button Content="▢"
Background="#FFBD44"
Style="{StaticResource UserControls}"
Click="Restore"/>
<Button Content="X"
Background="#FF605C"
Style="{StaticResource UserControls}"
Click="Exit"/>
</StackPanel>
</Grid>
<ContentControl Grid.Column="1"
Grid.Row="1"
Margin="20,10,20,50"
Content="{Binding CurrentView}"/>
</Grid>
</Border>
</Window>
MainViewModel.cs
using QuizAppV2.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace QuizAppV2.MVVM.ViewModel
{
class MainViewModel : ObservableObject
{
public RelayCommand LoginViewCommand { get; set; }
public RelayCommand SubjectSelectionViewCommand { get; set; }
public RelayCommand QuizViewCommand { get; set; }
public RelayCommand ResultViewCommand { get; set; }
public LoginViewModel LoginVM { get; set; }
public SubjectSelectionViewModel SubjectSelectVM { get; set; }
public QuizViewModel QuizVM { get; set; }
public ResultViewModel ResultVM { get; set; }
private object _currentView;
public object CurrentView
{
get { return _currentView; }
set
{
_currentView = value;
onPropertyChanged();
}
}
public MainViewModel()
{
LoginVM = new LoginViewModel();
SubjectSelectVM = new SubjectSelectionViewModel();
QuizVM = new QuizViewModel();
ResultVM = new ResultViewModel();
CurrentView = SubjectSelectVM;
LoginViewCommand = new RelayCommand(o =>
{
CurrentView = LoginVM;
});
SubjectSelectionViewCommand = new RelayCommand(o =>
{
CurrentView = SubjectSelectVM;
});
}
}
}
LoginView.xaml
using QuizAppV2.MVVM.ViewModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace QuizAppV2.MVVM.View
{
/// <summary>
/// Interaction logic for LoginView.xaml
/// </summary>
public partial class LoginView : UserControl
{
public LoginView()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (UsrId.Text == "" || UsrName.Text == "")
{
UsrIDErrMsg.Visibility = Visibility.Visible;
UsrNameErrMsg.Visibility = Visibility.Visible;
}
else
{
UsrIDErrMsg.Visibility = Visibility.Hidden;
UsrNameErrMsg.Visibility = Visibility.Hidden;
MainWindow.currentUser = new Student(UsrId.Text, UsrName.Text);
}
}
}
}
Thank you
I suggest using "Datatemplate". Put in the main window resources the following:
<DataTemplate DataType="{x:Type viewmodel:QuizViewModel}">
<local:QuizView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodel:LoginViewModel}">
<local:LoginView/>
</DataTemplate>
and so on with the others...
WPF is doing all the work for you, it examine the "CurrentView" prroperty and select how to view it according the the suitable DataTemplate.
Navigation is a tricky topic there are few ways to do this but since you are new to WPF I tried to outline a simple technique, along the lines of the examples you've provided requirement is have to go from page to page, a simple idea would be to swap out the contents. What I mean by that is when the user clicks "Login" we authenticate the user and swap the LoginPage with some other page, in your case a quiz page, when the user selection any option we swap out the view with the next view and so on.
I've coded up a simple solution with Shell mechanism. Essentially we create a empty shell in MainWindow (i.e it has no UI) and we load pages into this empty shell using a NavigationService/Helper. I've gone with a singleton class here just for simplicity, there are 3 main Methods in this,
RegisterShell : This has to be the Window where the swapping will happen, this ideally needs to be set once.
Load View : Method which Swaps out old view with the new one, I have gone with user control for this as most of the sub views can be user control in WPF.
LoadViewWithCustomData : Similar to above but has more flexibilty since it allows you to supply extra data.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace Navigation
{
class NavigationService
{
/// <summary>
/// Singleton so we keep on shell which views can use this to navigate to different pages.
/// </summary>
public static NavigationService Instance = new NavigationService();
private MainWindow myShell;
private NavigationService()
{
}
/// <summary>
/// Register the main shell so this service know where to swap the data out and in of
/// </summary>
/// <param name="theShell"></param>
public void RegisterShell(MainWindow theShell)
{
this.myShell = theShell;
}
/// <summary>
/// Swaps out any view to the shell.
/// </summary>
/// <typeparam name="T"></typeparam>
public void LoadView<T>() where T : UserControl, new()
{
myShell.TheShell = new T();
}
/// <summary>
/// Swaps out any view to the shell with custom data, here the user responsible to create UserControl with all the reqired data for the view.
/// We can automate this via reflection if required.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="theNewControl"></param>
public void LoadViewWithCustomData<T>(UserControl theNewControl) where T : UserControl, new()
{
myShell.TheShell = theNewControl;
}
}
Now here's how my LoginPage looks, the important line here is NavigationService.Instance.LoadView<_4OptionQuizPage>() this essentially sends the user to _4OptionQuizPage.
public partial class LoginPage : UserControl
{
public ICommand LoginClicked { get; }
public LoginPage()
{
InitializeComponent();
this.DataContext = this;
LoginClicked = new SimpleCommand(OnLoginClicked);
}
private void OnLoginClicked()
{
// TODO : Authenticate user here.
// Send the user to Quiz Page
NavigationService.Instance.LoadView<_4OptionQuizPage>();
}
}
And in the _4OptionQuizPage we can have something like this, this is where the bulk of business logic may reside, I have 4 buttons here, 2 of them show message box but Button 1 sends you back to LoginPage and Button 2 reloads the same page with different data (i.e sending the user to next question)
public partial class _4OptionQuizPage : UserControl, INotifyPropertyChanged
{
public ICommand Option1Clicked { get; }
public ICommand Option2Clicked { get; }
public ICommand Option3Clicked { get; }
public ICommand Option4Clicked { get; }
private string myQuestion;
public event PropertyChangedEventHandler PropertyChanged;
public string Question
{
get { return myQuestion; }
set
{
myQuestion = value;
NotifyPropertyChanged();
}
}
public _4OptionQuizPage() : this($"Question Loaded At {DateTime.Now}, this can be anything.")
{
}
public _4OptionQuizPage(string theCustomData)
{
InitializeComponent();
Question = theCustomData;
this.DataContext = this;
this.Option1Clicked = new SimpleCommand(OnOption1Clicked);
this.Option2Clicked = new SimpleCommand(OnOption2Clicked);
this.Option3Clicked = new SimpleCommand(OnOption3Clicked);
this.Option4Clicked = new SimpleCommand(OnOption4Clicked);
}
private void OnOption4Clicked()
{
MessageBox.Show("Option 4 selected, Store the results");
}
private void OnOption3Clicked()
{
MessageBox.Show("Option 3 selected, Store the results");
}
private void OnOption1Clicked()
{
NavigationService.Instance.LoadView<LoginPage>();
}
private void OnOption2Clicked()
{
NavigationService.Instance.LoadViewWithCustomData<LoginPage>(new _4OptionQuizPage("A custom question to emulate custom data"));
}
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Finally your MainWindow would be registering the shell and sending the user to LoginPage, and it's XAML file should not have anything in it
public partial class MainWindow : Window, INotifyPropertyChanged
{
private object myShell;
public object TheShell
{
get { return myShell; }
set
{
myShell = value;
this.NotifyPropertyChanged();
}
}
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
NavigationService.Instance.RegisterShell(this);
NavigationService.Instance.LoadView<LoginPage>();
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
MainWindow.xaml should be empty, essentially a shell for everything else.
<Window x:Class="Navigation.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Navigation"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" Content="{Binding TheShell}">
</Window>
This sample demonstrates two approaches to navigation. Often useful since you say want to start by logging in but not show any menus etc until the user is logged in. Then once they log in you want some sort of menu or list of views they can navigate to which remains static.
My mainwindow is purely a shell to contain everything.
It's markup is:
<Window ......
Title="{Binding Title}"
Content="{Binding}"
/>
This sample uses viewmodel first for all navigation. Viewmodels are templated out into UI.
There is more in the code behind.
public partial class LoginNavigationWindow : Window
{
public Type ParentViewModel
{
get { return (Type)GetValue(ParentViewModelProperty); }
set { SetValue(ParentViewModelProperty, value); }
}
public static readonly DependencyProperty ParentViewModelProperty =
DependencyProperty.Register(name: "ParentViewModel",
propertyType: typeof(Type),
ownerType: typeof(LoginNavigationWindow),
typeMetadata: new FrameworkPropertyMetadata(
defaultValue: null,
propertyChangedCallback: new PropertyChangedCallback(ParentViewModelChanged)
));
private static void ParentViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var vm = Activator.CreateInstance((Type)e.NewValue);
((Window)d).DataContext = vm;
Task.Run(((IInitiatedViewModel)vm).Initiate);
}
public LoginNavigationWindow()
{
InitializeComponent();
WeakReferenceMessenger.Default.Register<ParentNavigate>(this, (r, pn) =>
{
this.SetValue(LoginNavigationWindow.ParentViewModelProperty, pn.ParentViewModelType);
});
}
The messenger registration will switch out the window's datacontext using a dependency property. The message is just a class with a property to pass a Type
public class ParentNavigate
{
public Type ParentViewModelType { get; set; }
}
The callback ParentViewModelChanged takes a type, instantiates it and sets datacontext on the window.
Usually, you're not interested in retaining state of a window or parent level piece of view. You already logged in. If you wanted to log back in again then you would start again and input name and password.
The entrypoint is slightly unusual since I handle application startup and rely on that dependency property callback.
private void Application_Startup(object sender, StartupEventArgs e)
{
var mw = new LoginNavigationWindow();
mw.Show();
mw.SetValue(LoginNavigationWindow.ParentViewModelProperty, typeof(LoginViewModel));
}
Instead of a mainwindow full of menus etc I have of course got nothing.
I have a LoginUC is the first thing you will see on start up. This is just illustrative.
We will get input from the user and validate it before navigating in a real app. We're just interested in that navigation here so this version just has a button to navigate to MainViewModel:
<Grid>
<StackPanel>
<TextBlock Text="Log in"/>
<Button Content="Go"
Command="{Binding LoadMainCommand}"/>
</StackPanel>
</Grid>
</UserControl>
My LoginViewModel has a command, title and a task.
public partial class LoginViewModel : BaseParentViewModel
{
[RelayCommand]
private async Task LoadMain()
{
var pn = new ParentNavigate{ ParentViewModelType = typeof(MainViewModel) };
WeakReferenceMessenger.Default.Send(pn);
}
public LoginViewModel()
{
Title = "Please Log In first";
}
public override async Task Initiate()
{
// Get any data for login here
}
}
BaseParentViewModel
public partial class BaseParentViewModel : ObservableObject, IInitiatedViewModel
{
[ObservableProperty]
private string title = string.Empty;
virtual public async Task Initiate() { }
}
Interface
public interface IInitiatedViewModel
{
Task Initiate();
}
The purpose of this interface is to give us a generic way for any viewmodel to get any data it requires. By setting datacontext and then starting up a background thread to get that data the view will appear quickly and then fill with any data it needs. If getting that data takes a while then at least the view is "up" and visible quickly whilst the task still carries on working.
In a fuller example we would have IsBusy in a base viewmodel which would start off true and be changed to false. That would drive a "throbber" or busing indicator in the view.
A resource dictionary associates viewmodel datatemplates with usercontrols using datatype:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:LoginNavigation"
>
<DataTemplate DataType="{x:Type local:MainViewModel}">
<local:MainUC/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:LoginViewModel}">
<local:LoginUC/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:SubjectsViewModel}">
<local:SubjectsView/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ResultViewModel}">
<local:ResultView/>
</DataTemplate>
</ResourceDictionary>
That is merged in app.xaml
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Resources/ViewDataTemplates.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Once you login, the entire content of the window is replaced. The datacontext is changed from LoginViewModel to MainViewModel, that is then templated out into MainUC:
public partial class MainViewModel : BaseParentViewModel
{
[ObservableProperty]
private object currentChildViewModel;
[ObservableProperty]
private List<ChildViewModel> childViewModelList;
[RelayCommand]
private async Task ChildNavigation(ChildViewModel cvm)
{
if (cvm.Instance == null)
{
cvm.Instance = Activator.CreateInstance(cvm.ViewModelType);
if (cvm.Instance is IInitiatedViewModel)
{
Task.Run(((IInitiatedViewModel)cvm.Instance).Initiate);
}
}
CurrentChildViewModel = cvm.Instance;
}
public override async Task Initiate()
{
ChildViewModelList = new List<ChildViewModel>()
{
new ChildViewModel{ DisplayName="Subjects", ViewModelType= typeof(SubjectsViewModel) },
new ChildViewModel{ DisplayName="Results", ViewModelType= typeof(ResultViewModel) }
};
}
public MainViewModel()
{
Title = "Quiz";
}
}
You would probably want to have more views of course and pick one to show initially which would be setup in Initiate.
MainUC:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ListBox ItemsSource="{Binding ChildViewModelList}"
HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Content="{Binding DisplayName}"
Command="{Binding DataContext.ChildNavigationCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ContentPresenter Content="{Binding CurrentChildViewModel}"
Grid.Column="1"/>
</Grid>
</UserControl>
In the view you get a list of buttons in a left column which will allow navigation in the right column. But retaining MainUC of course.
Instead of a listbox this could be a menu or maybe a tabcontrol.
Clicking on a button calls a command in MainViewModel and passes the instance of ChildViewModel as a parameter.
That is then used to instantiate a viewmodel, set CurrentChildViewmodel and cache the instance.
CurrentChildViewmodel will of course itself be templated out into a usercontrol within MainUC.
public partial class ChildViewModel : ObservableObject
{
public string DisplayName { get; set; }
public Type ViewModelType { get; set; }
public object Instance { get; set; }
}
This is rather a simplistic approach and in a real world substantial app you would want dependency injection, factories and the like. But this is already quite a bit of code for a Stack Overflow answer as it is.
The remaining viewmodels and views are just simplistic implementations to prove it all works. eg
public partial class SubjectsViewModel : ObservableObject, IInitiatedViewModel
{
public async Task Initiate()
{
// Get any data for Subjects here
}
}
and
<Grid>
<TextBlock Text="Subjects"/>
</Grid>
</UserControl>
There are many ways how to allow a view model to participate in page navigation.
In general, each class that participates in navigation has to have access to your navigation API.
For example, you could move the navigation logic to a dedicated class NavigationService and provide a shared reference to every class that should be able to navigate to a different view.
Alternatively (and recommended), you can use routed commands that you handle on the MainWindow, which then delegates the command to the MainViewModel.
In this scenario each button would have to pass the destination as CommandParameter. This solution allows the particular view models to not directly participate in the navigation. You don't need to pollute your view model classes with navigation details.
The following example shows how to navigate from the QuizView to the ResultView using a RoutedCommand.
MainViewModel.cs
The MainViewModel is the only view model class that knows how to navigate and about the related details.
This enables extensibility while keeping the implementation of the view model classes simple.
In general, to enable data validation let the view models implement INotifyDataErrorInfo.
You can then query the INotifyDataErrorInfo.HasErrors property before allowing to leave a page.
class MainViewModel : ObservableObject
{
public object CurrentView { get; set; }
private Dictionary<Type, INotifyPropertyChanged> ViewModelMap { get; }
public MainViewModel()
{
this.ViewModelMap = new Dictionary<Type, INotifyPropertyChanged>
{
{ typeof(QuizVm), new QuizVm() },
{ typeof(ResultVm), new ResultVm() },
};
}
// Check if destination type is valid.
// In case the navigation source implements INotifyDataErrorInfo,
// check if the source is in a valid state (INotifyDataErrorInfo.HasEWrrors returns 'false').
// This method is called by the view. It will delegate its ICommand.CanExecute to this method
// If this method returns 'false' the command source e.g. Button will be disabled.
public bool CanNavigate(Type navigationSourceType, Type navigationDestinationType)
=> CanNavigateAwayFrom(navigationSourceType)
&& CanNavigateTo(navigationDestinationType);
private bool CanNavigateAwayFrom(Type navigationSourceType)
=> this.ViewModelMap.TryGetValue(navigationSourceType, out INotifyPropertyChanged viewModel)
&& viewModel is INotifyDataErrorInfo notifyDataErrorInfo
? !notifyDataErrorInfo.HasErrors
: true;
private bool CanNavigateTo(Type navigationDestinationType)
=> this.ViewModelMap.ContainsKey(navigationDestinationType);
// This method is called by the view. It will delegate its ICommand.Execute to this method
public void NavigateTo(Type destinationType)
{
if (this.ViewModelMap.TryGetValue(destinationType, out INotifyPropertyChanged viewModel))
{
this.CurrentView = viewModel;
}
}
}
MainWindow.xaml.cs
partial class MainWindow : Window
{
public static RoutedCommand NavigateCommand { get; } = new RoutedUICommand(
"Navigate to view command",
nameof(NavigateCommand),
typeof(MainWindow));
private MainViewModel MainViewModel { get; }
public MainWindow()
{
InitializeComponent();
this.MainViewModel = new MainViewModel();
this.DataContext = this.MainViewModel;
var navigateCommandBinding = new CommandBinding(MainWindow.NavigateCommand, ExecuteNavigateCommand, CanExecuteNavigateCommand);
this.CommandBindings.Add(navigateCommandBinding);
}
private void CanExecuteNavigateCommand(object sender, CanExecuteRoutedEventArgs e)
{
if (e.Source is not FrameworkElement commandSource)
{
return;
}
Type navigationSourceType = commandSource.DataContext.GetType();
Type navigationDestinationType = (Type)e.Parameter;
e.CanExecute = this.MainViewModel.CanNavigate(navigationSourceType, navigationDestinationType);
}
private void ExecuteNavigateCommand(object sender, ExecutedRoutedEventArgs e)
{
var destinationViewModelType = (Type)e.Parameter;
this.MainViewModel.NavigateTo(destinationViewModelType);
}
}
MainWindow.xaml
To actually render the views (for example a custom Control) you need to define an implicit DataTemplate (without the x:Key directive) that has the associated view model class as DataType. The ContentControl will then automatically pick the correct one that matches the type of the ContentControl.Content property value.
<Window>
<Window.Resources>
<DataTemplate DataType="{x:Type local:QuizVM}">
<QuizView />
</DataTemplate>
<DataTemplate DataType="{x:Type local:ResultVM}">
<ResultView />
</DataTemplate>
</Window.Resources>
<ContentControl Content="{Binding CurrentView}" />
</Window>
If a view needs to navigate, it must use the static routed command (defined and handled in the MainWindow) and pass the Type of the destination view model as CommandParameter.
This way, navigation will not pollute the view models and stays within the view.
QuizView.xaml
<QuizView>
<Button Content="Next"
Command="{x:Static local:MainWindow.NextPageCommand}"
CommandParameter="{x:Type local:ResultVM}"/>
</QuizView>
ResultView.xaml
<ResultView>
<Button Content="Back"
Command="{x:Static local:MainWindow.NextPageCommand}"
CommandParameter="{x:Type local:QuizVM}"/>
</ResultView>
Because the view model classes generally don't directly participate in the navigation,
they don't have to implement any related commands or depend on any navigation service.
Navigation is completely controlled by the MainWindow and its MainViewModel.
For optional data validation let them implement INotifyDataErrorInfo.
QuizVM.cs
class QuizVM : INotifyPropertyChnaged, INotifyDataErrorInfo
{
}
ResultVM.cs
class ResultVM : INotifyPropertyChnaged, INotifyDataErrorInfo
{
}

Binding tab controls with mahapps and prism - WPF

I am building a WPF application with mahapps, prism[modularity]. I have below HomeWindow.xaml code.
<Controls:MetroWindow x:Class="Project.Views.HomeWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:local="clr-namespace:Project.Views"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
<!--The above code is for automatically binding of viewmodel into view-->
Height="700" Width="1200" Background="White">
<Grid>
<TabControl ItemsSource="{Binding TabCollection}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock Text="{Binding Name}"/>
</TextBlock>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<Label Content="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Controls:MetroWindow>
I have below structure in my HomeViewModel.cs under ViewModels directory.
public class HomeViewModel : BindableBase
{
private ObservableCollection<Item> _tabCollection;
public ObservableCollection<Item> TabCollection { get { return _tabCollection; } set { SetProperty(ref _tabCollection, value); } }
//Prism way of getting and setting data
}
public class Item
{
private string Name;
private string Content;
public Item(string name, string content)
{
Name = name;
Content = content;
}
}
below is how I add data into TabCollection property through HomeWindow.xaml.cs.
private HomeViewModel _model=new HomeViewModel();
public HomeWindow(EmployeeViewModel model)
{
InitializeComponent();
_model.UserViewModel = model;
LoadHomeData(_model.UserViewModel.EmpRole);
DataContext = this;
}
private void LoadHomeData(string Role)
{
if (string.Equals(Role, "Admin"))
{
_model.TabCollection= new ObservableCollection<Item>()
{
new Item("Test1", "1"),
new Item("Test2", "2"),
new Item("Test3", "3")
};
}
}
Now matter what, the tabs will not get displayed. Its a blank empty window. I have followed the example in the issue here and have went through few similar posts having same kind of approach. But none of them helped. Is this because of prism way of databinding or is there anything else am missing here? Hope to find some help on this..
Your problem is not connected to MahApps or Prism but to how WPF works in general. In your case Name and Content are private fields and should be public properties
public string Name { get; set; }
public string Content { get; set; }
private or field is not a valid binding source. You can find more as to what is a valid binding source under Binding Sources Overview but in your case, as far as CLR object goes:
You can bind to public properties, sub-properties, as well as indexers, of any common language runtime (CLR) object. The binding engine uses CLR reflection to get the values of the properties. Alternatively, objects that implement ICustomTypeDescriptor or have a registered TypeDescriptionProvider also work with the binding engine.
Another problem is that DataContext is set wrong. At the moment is set to HomeWindow and I think it should be set to instance of HomeViewModel which holds TabCollection property
DataContext = _model;

What's the issue with the way I'm binding to a dependency property?

Note: You can find the project below on github now. https://github.com/ReasonSharp/MyTestRepo
I'm creating a simple list control with a scrollbar that will display a collection of objects I pass to it. When a user clicks on one item, I want it to become a selected item, and when he clicks it again, I want it to be unselected. I store the selected item in a SelectedLocation property. While debugging, the property is set appropriately. However, if I place this list control (LocationListView) onto a window and bind to SelectedLocation (like SelectedLocation="{Binding MyLocation}") in a control, the binding won't work, and if I try to use this MyLocation in another binding in the same window (i.e. <TextBox Text="{Binding MyLocation.ID}"/>, where ID is a dependency property), that binding won't show anything changing as I select different items in the list.
Minimal example is a bit large, please bear with me:
List control
XAML
<UserControl x:Class="MyListView.LocationListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MyListView"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid x:Name="locationListView">
<ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="myStackPanel"/>
</ScrollViewer>
</Grid>
</UserControl>
Code behind
using System.Collections;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
namespace MyListView {
public partial class LocationListView : UserControl {
#region Dependency Properties
public IEnumerable Locations {
get { return (IEnumerable)GetValue(LocationsProperty); }
set { SetValue(LocationsProperty, value); }
}
public static readonly DependencyProperty LocationsProperty =
DependencyProperty.Register("Locations", typeof(IEnumerable), typeof(LocationListView), new PropertyMetadata(null, LocationsChanged));
public MyObject SelectedLocation {
get { return (MyObject)GetValue(SelectedLocationProperty); }
set { SetValue(SelectedLocationProperty, value); }
}
public static readonly DependencyProperty SelectedLocationProperty =
DependencyProperty.Register("SelectedLocation", typeof(MyObject), typeof(LocationListView), new PropertyMetadata(null));
#endregion
private static void LocationsChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) {
((LocationListView)o).RegenerateLocations();
if (((LocationListView)o).Locations is ObservableCollection<MyObject>) {
var l = ((LocationListView)o).Locations as ObservableCollection<MyObject>;
l.CollectionChanged += ((LocationListView)o).L_CollectionChanged;
}
}
private void L_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) {
RegenerateLocations();
}
private Button selectedLV = null;
public LocationListView() {
InitializeComponent();
}
private void RegenerateLocations() {
if (Locations != null) {
myStackPanel.Children.Clear();
foreach (var l in Locations) {
var b = new Button();
b.Content = l;
b.Click += B_Click;
myStackPanel.Children.Add(b);
}
}
selectedLV = null;
}
private void B_Click(object sender, RoutedEventArgs e) {
var lv = (sender as Button)?.Content as MyObject;
if (selectedLV != null) {
lv.IsSelected = false;
if ((selectedLV.Content as MyObject) == SelectedLocation) {
SelectedLocation = null;
selectedLV = null;
}
}
if (lv != null) {
SelectedLocation = lv;
selectedLV = sender as Button;
lv.IsSelected = true;
}
}
}
}
Note the absence of this.DataContext = this; line. If I use it, I get the following binding expression path errors:
System.Windows.Data Error: 40 : BindingExpression path error: 'SillyStuff' property not found on 'object' ''LocationListView' (Name='')'. BindingExpression:Path=SillyStuff; DataItem='LocationListView' (Name=''); target element is 'LocationListView' (Name=''); target property is 'Locations' (type 'IEnumerable')
System.Windows.Data Error: 40 : BindingExpression path error: 'MySelectedLocation' property not found on 'object' ''LocationListView' (Name='')'. BindingExpression:Path=MySelectedLocation; DataItem='LocationListView' (Name=''); target element is 'LocationListView' (Name=''); target property is 'SelectedLocation' (type 'MyObject')
Using (this.Content as FrameworkElement).DataContext = this; won't produce these errors, but it won't work either.
Main window
XAML
<Window x:Class="MyListView.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MyListView"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DockPanel LastChildFill="True" HorizontalAlignment="Stretch" VerticalAlignment="Top">
<local:LocationListView Locations="{Binding SillyStuff}" SelectedLocation="{Binding MySelectedLocation}" DockPanel.Dock="Top"/>
<TextBox Text="{Binding MySelectedLocation.ID}" DockPanel.Dock="Top"/>
</DockPanel>
</Grid>
</Window>
Code behind
using System.Windows;
using Microsoft.Practices.Unity;
namespace MyListView {
public partial class MainWindow : Window {
private MainViewModel vm;
public MainWindow() {
InitializeComponent();
}
[Dependency] // Unity
internal MainViewModel VM {
set {
this.vm = value;
this.DataContext = vm;
}
}
}
}
MainViewModel
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace MyListView {
class MainViewModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs e) {
if (PropertyChanged != null)
PropertyChanged(sender, e);
}
private MyObject mySelectedLocation;
public MyObject MySelectedLocation {
get { return mySelectedLocation; }
set {
mySelectedLocation = value;
OnPropertyChanged(this, new PropertyChangedEventArgs("MySelectedLocation"));
}
}
public ObservableCollection<MyObject> SillyStuff {
get; set;
}
public MainViewModel() {
var cvm1 = new MyObject();
cvm1.ID = 12345;
var cvm2 = new MyObject();
cvm2.ID = 54321;
var cvm3 = new MyObject();
cvm3.ID = 15243;
SillyStuff = new ObservableCollection<MyObject>();
SillyStuff.Add(cvm1);
SillyStuff.Add(cvm2);
SillyStuff.Add(cvm3);
}
}
}
MyObject
using System.Windows;
namespace MyListView {
public class MyObject : DependencyObject {
public int ID {
get { return (int)GetValue(IDProperty); }
set { SetValue(IDProperty, value); }
}
public static readonly DependencyProperty IDProperty =
DependencyProperty.Register("ID", typeof(int), typeof(MyObject), new PropertyMetadata(0));
public bool IsSelected {
get; set;
}
public override string ToString() {
return ID.ToString();
}
}
}
App.xaml -- just to save anyone the typing
XAML
<Application x:Class="MyListView.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyListView">
<Application.Resources>
</Application.Resources>
</Application>
Code behind
using System.Windows;
using Microsoft.Practices.Unity;
namespace MyListView {
public partial class App : Application {
protected override void OnStartup(StartupEventArgs e) {
base.OnStartup(e);
UnityContainer container = new UnityContainer();
var mainView = container.Resolve<MainWindow>();
container.Dispose();
mainView.Show();
}
}
}
The objective here is to have the value in the TextBox on MainWindow change to the selected item's ID whenever the selected item changes. I could probably do it by creating a SelectedItemChanged event on my LocationListView, and then setting the property manually in a handler, but that seems like a hack. If you place a <ListView ItemsSource="{Binding SillyStuff}" SelectedItem="{Binding MySelectedLocation}" DockPanel.Dock="Top"/> instead of my list control, this works like a charm, so I should be able to make my control work that way too.
Edit: Changed MainViewModel to implement INotifyPropertyChanged as per Pieter's instructions.
Main issues
When you select an item in your custom control, B_Click assigns it to the SelectedLocation property, which calls SetValue internally. However, this overwrites the binding on SelectedLocation - in other words, after that call SelectedLocation is no longer bound to anything. Use SetCurrentValue instead to preserve the binding.
However, bindings won't update their source by default. You'll have to set their Mode to TwoWay. You can do that in XAML: SelectedLocation="{Binding MySelectedLocation, Mode=TwoWay}", or mark the dependency property to use TwoWay binding by default: new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, LocationsChanged).
Finally, make sure that your binding paths are correct. Your text box binds to SelectedLocation, while the property is named MySelectedLocation. These kind of issues are usually logged in the debug output, in this case you should get a message like this:
System.Windows.Data Error: 40 : BindingExpression path error: 'SelectedLocation' property not found on 'object' ''MainViewModel' (HashCode=8757408)'. BindingExpression:Path=SelectedLocation.ID; DataItem='MainViewModel' (HashCode=8757408); target element is 'TextBox' (Name=''); target property is 'Text' (type 'String')
Other issues
I've found a few other issues as well: you're not unregistering L_CollectionChanged when another collection is set, and if the collection is removed, you're not clearing the visible items. The code in B_Click is also troublesome: you're also accessing lv before making sure it's not null, and if the user clicks on an unselected button you're setting SelectedLocation to null before setting it to the newly selected item. Also, when regenerating items, selectedLV (what's 'lv'?) is set to null, but SelectedLocation is left intact...
Also a little tip: your OnPropertyChanged method only needs a single argument: string propertyName. Make it optional and mark it with a [CallerMemberName] attribute, so all that a property setter needs to do is call it without arguments. The compiler will insert the calling property name for you.
Alternatives
Personally, I'd just use a ListView with a custom ItemTemplate:
<ListView ItemsSource="{Binding MyLocations}" SelectedItem="{Binding MySelectedLocation}" SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate>
<ToggleButton IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListViewItem}}" Content="{Binding}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
This probably requires a few more modifications to make it look nice, but that's the gist of it. Alternately, you could create an attached behavior that takes care of your desired selection behavior.
Oh boy, that's a lot of code.
Let me begin by highlighting a common mistake, which is setting the control's DataContext to itself. This should be avoided as it tends to screw up absolutely everything.
So. Avoid doing this:
this.DataContext = this;
It is not the responsibility of the UserControl itself to set it's own DataContext, it should be the responsibility of the parent control (such as a Window to set it. Like this:
<Window ...>
<local:MyUserControl DataContext="{Binding SomeProperty}" ... />
If your UserControl was to set its own DataContext, then it will override what the Window sets its DataContext to be. Which will result in the screwing up of absolutely everything.
To bind to a Dependency Property of a UserControl, simply give your control an x:Name and use an ElementName binding, like this:
<UserControl ...
x:Name="usr">
<TextBlock Text="{Binding SomeDependencyProperty, ElementName=usr}" ... />
What's important to note here is that the DataContext isn't being set at all, so your parent Window is free to set the control's DataContext to whatever it needs to be.
Adding to this, your UserControl can now bind to it's DataContext using a straightforward Path binding.
<UserControl ...
x:Name="usr">
<TextBlock Text="{Binding SomeDataContextProperty}" ... />
I hope this helps.

Bind Data From Property to Textblock - MVVM Light and WPF

I have a textblock in WPF which is bound to a property in my ViewModel class. On click of a button I wish to modify the property and expect the same to be reflected in my textblock. I want all these to be done purely using MVVM (MVVMLight). I am using MMVM light and VS 2012.
Challenges- On button click the changes are not being reflected. Though the program execution is going inside the property , changes are not being made.
Please Help !!
Program- View:
<Window x:Class="MvvmLight1_Trail.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ignore="http://www.ignore.com"
mc:Ignorable="d ignore"
Height="500"
Width="500"
Title="MVVM Light Application"
DataContext="{Binding Main, Source={StaticResource Locator}}">
<Grid x:Name="LayoutRoot">
<TextBlock FontSize="34"
Text="{Binding Path=MyText,UpdateSourceTrigger=Default, Mode=TwoWay}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextWrapping="Wrap" />
<Button Width="100" Height="100" Command="{Binding PressCommand}" Margin="198.985,277.537,193.014,92.462" Content="Press Me"/>
</Grid>
View Model
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using MvvmLight1_Trail.Model;
using System.ComponentModel;
using System.Threading;
namespace MvvmLight1_Trail.ViewModel
{
public class MainViewModel : ViewModelBase
{
public RelayCommand PressCommand { get; private set; }
Thread t;
private string _welcomeTitle = string.Empty;
public string MyText
{
get
{
return _welcomeTitle;
}
set
{
if (_welcomeTitle == value)
{
return;
}
_welcomeTitle = value;
RaisePropertyChanged(MyText);
}
}
/// <summary>
/// Initializes a new instance of the MainViewModel class.
/// </summary>
public MainViewModel()
{
PressCommand = new RelayCommand(() => MyFunc());
myfunc();
}
private void MyFunc()
{
this.MyText = "Hi2";
}
private void myfunc()
{
this.MyText = "Hello";
this.MyText = "Hi";
}
}
}
Replace
RaisePropertyChanged(MyText);
to
RaisePropertyChanged("MyText");
PropertyChanged event should be raised on property name and not on property value.
Already answered by #Rohit Vats. You can also call RaisePropertyChanged like, RaisePropertyChanged( () => MyText) to ease renaming later.
Late to the game but:
in new C# 6 you can also use nameof like this:
RaisePropertyChanged(nameof(MyText))

My WPF custom control's Data Context is superseding parent's

In my main window, I try to bind to a bool, but it's looking in my custom control's DataContext instead. If I don't assign DataContext in the user control, then the main window's bindings works, but (obviously) this brakes the bindings in the user control.
Here's the error:
System.Windows.Data Error: 40 : BindingExpression path error: 'MyControlVisible' property not found on 'object' ''MyUserControlModel' (HashCode=1453241)'. BindingExpression:Path=MyControlVisible; DataItem='MyUserControlModel' (HashCode=1453241); target element is 'MyUserControl' (Name='_myUserControl'); target property is 'Visibility' (type 'Visibility')
I need binding to work on both controls, but I don't want the user control's DataContext to supersede the window's.
Here's the code:
<Window x:Class="Sandbox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Controls="clr-namespace:Sandbox.Controls" Title="Sandbox">
<DockPanel LastChildFill="True">
<DockPanel.Resources>
<BooleanToVisibilityConverter x:Key="boolToVis" />
</DockPanel.Resources>
<Grid>
<Controls:MyUserControl x:Name="_myUserControl" Visibility="{Binding MyControlVisible, Converter={StaticResource boolToVis}}"/>
</Grid>
</DockPanel>
</Window>
namespace Sandbox
{
public partial class MainWindow
{
private MainWindowModel model;
public MainWindow()
{
InitializeComponent();
DataContext = model = new MainWindowModel();
_myUserControl.Initialize(model.MyUControlModel);
}
}
}
using System.ComponentModel;
using Sandbox.Controls;
namespace Sandbox
{
public class MainWindowModel : BaseModel
{
public MyUserControlModel MyUControlModel { get; set; }
public bool MyControlVisible { get; set; }
public MainWindowModel()
{
MyUControlModel = new MyUserControlModel();
MyControlVisible = false;
OnChange("");
}
}
public class BaseModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnChange(string s)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(s));
}
}
}
}
<UserControl x:Class="Sandbox.Controls.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d">
<Grid>
<TextBlock Text="{Binding MyBoundText}"/>
</Grid>
</UserControl>
namespace Sandbox.Controls
{
public partial class MyUserControl
{
public MyUserControl()
{
InitializeComponent();
}
public void Initialize(MyUserControlModel context)
{
DataContext = context;
}
}
}
namespace Sandbox.Controls
{
public class MyUserControlModel : BaseModel
{
public string MyBoundText { get; set; }
public MyUserControlModel()
{
MyBoundText = "Hello World!";
OnChange("");
}
}
}
That is one of the many reasons you should never set the DataContext directly from the UserControl itself.
When you do so, you can no longer use any other DataContext with it because the UserControl's DataContext is hardcoded in.
In the case of your binding, normally the DataContext would be inherited so the Visibility binding could find the property MyControlVisible on the current DataContext, however because you hardcoded the DataContext in your UserControl's constructor, that property is not found.
You could specify a different binding source in your binding, such as
<Controls:MyUserControl Visibility="{Binding
RelativeSource={RelativeSource AncestorType={x:Type Window}},
Path=DataContext.MyControlVisible,
Converter={StaticResource boolToVis}}" ... />
However that's just a workaround for the problem for this specific case, and in my view is not a permanent solution. A better solution is to simply not hardcode the DataContext in your UserControl
There are a few different ways you can do depending on your UserControl's purpose and how your application is designed.
You could create a DependencyProperty on your UserControl to pass in the value, and bind to that.
<Controls:MyUserControl UcModel="{Binding MyUControlModelProperty}" ... />
and
<UserControl x:Class="Sandbox.Controls.MyUserControl"
ElementName=MyUserControl...>
<Grid DataContext="{Binding UCModel, ElementName=MyUserControl}">
<TextBlock Text="{Binding MyBoundText}"/>
</Grid>
</UserControl>
Or you could build your UserControl with the expectation that a specific property will get passed to it in the DataContext. This is normally what I do, in combination with DataTemplates.
<Controls:MyUserControl DataContext="{Binding MyUControlModelProperty}" ... />
and
<UserControl x:Class="Sandbox.Controls.MyUserControl"...>
<Grid>
<TextBlock Text="{Binding MyBoundText}"/>
</Grid>
</UserControl>
As I said above, I like to use DataTemplates to display my UserControls that expect a specific type of Model for their DataContext, so typically my XAML for the main window would look something like this:
<DataTemplate DataType="{x:Type local:MyUControlModel}">
<Controls:MyUserControl />
</DataTemplate>
<ContentPresenter Content="{Binding MyUControlModelProperty}" ... />

Categories

Resources