Bind WinUI 3 ContentDialog DataContext to Property - c#

I am using a custom ContentDialog in a WinUI 3 application to get information from the user. I want to use a property in the content dialog as its data source, but I can't work it out.
Here's a trimmed down version of the dialog's markup:
<ContentDialog
x:Class="Question.CustomContentDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Question"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DataContext="{Binding OurModel, RelativeSource={RelativeSource.Self}}"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
PrimaryButtonText="OK">
<Grid>
<TextBox Text="{Binding Name, Mode=TwoWay}"/>
</Grid>
</ContentDialog>
And the code for the dialog and data classes:
using Microsoft.UI.Xaml.Controls;
using System.ComponentModel;
namespace Question
{
public class Model : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string _name = string.Empty;
public string Name
{
get => _name;
set
{
_name = value;
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}
public sealed partial class CustomContentDialog : ContentDialog
{
public Model OurModel { get; set; } = new() { Name = "Foo" };
public CustomContentDialog()
{
this.InitializeComponent();
}
}
}
The dialog class is used like this:
{
var dialog = new CustomContentDialog
{
Title = "This is a test",
XamlRoot = Content.XamlRoot,
};
dialog.OurModel.Name = "Test";
var _ = dialog.ShowAsync();
}
My expectation would be that the attribute, d:DataContext="{Binding OurModel, RelativeSource={RelativeSource.Self}}" would set the DataContext of the ContentDialog to the OurModel property of the object itself -- and Intellisense in the Xaml file seems to bear that out -- but no binding is actually created. If it were, the text box would be set to either "Test" or "Foo", and the value of dialog.OurModel would reflect the user's changes when dialog.ShowAsync() returned, wouldn't it? That's what I'm hoping to accomplish.
I managed to work around this by creating a binding in the xaml file
<ContentDialog.DataContext>
<models:Model x:Name="TheModel"/>
</ContentDialog.DataContext>
but that creates a private property, TheModel in the class, which can't be set externally without a lot of extra rigmarole.

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
{
}

Opening second window from main window in WPF/MVVM app

Could you tell me how to in pure MVVM way call (I mean open/show) child window from parent window. Let's say I have two Views:
MainWindow.cs (MainWindow.xaml) - parent window (DataContext = new MainWindowViewModel())
Window.cs (Window.xaml) - child window (DataContext = new WindowViewModel())
And corresponding ViewModel classes:
MainWindowViewModel.cs
WindowViewModel.cs
I would like my window to be opened after button click (button that is on the MainWindow view). Because of that I have defined command binding in MainWindow.xaml:
<Button x:Name="buttonOpenWindow" Content="Open window..." Width="100" Height="20" Command="{Binding OpenWindowCmd}"/>
And MainWindowViewModel.cs piece:
public ICommand OpenWindowCmd { get; set; }
public MainWindowViewModel()
{
OpenWindowCmd = new RelayCommand(o => OpenWindow());
}
private void OpenWindow()
{
// What to put here?
}
In Window.xaml I added something like that:
<Window x:Class="Namespace.View.Window"
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"
mc:Ignorable="d"
xmlns:vm="clr-namespace:Namespace.ViewModel"
Title="Title" Height="300" Width="325" Visibility="{Binding IsWindowVisible, Converter={StaticResource BooleanToVisibilityConverter}}">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</Window.Resources>
(...)
And the WindowViewModel.cs:
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Namespace.Annotations;
namespace Namespace.ViewModel
{
public class WindowViewModel : INotifyPropertyChanged
{
private bool _isWindowVisible;
public bool IsWindowVisible
{
get { return _isWindowVisible; }
set
{
_isWindowVisible = value;
OnPropertyChanged(nameof(IsWindowVisible));
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I am not sure what to do next and if that approach is correct. I found some services implementations in the forum, but I thought of using just Visibility property instead (but not sure if it is possible). I need to somehow change the IsWindowVisible in one of the view models I suppose. Could anyone suggest how to gently handle such sub window opening?
If I understood well, you need something like this:
private void OpenWindow()
{
WindowViewModel wvm = new WindowViewModel();
Window win = new Window()
{
DataContext = wvm;
};
win.Show();
}
If you don't like this solution then try the one from the comments with IWindowService.
In any case it makes no sense to use a Visibility property.

Bind SelectedValue of UserControl to ViewModel

In my solution; I have two projects: One is a WPF UserControl Library, and the other is a WPF Application.
The usercontrol is pretty straightforward; it's a label and a combo box that will show the installed printers.
In the WPF application; I want to use this usercontrol. The selected value will be stored in user settings.
The problem I'm having is that I can't seem to get the proper binding to work. What I need to happen is to be able to set the SelectedValue of the UserControl when the MainWindow loads; as well as access the SelectedValue of the UserControl when I go to save my settings.
My code is below, could someone point me in the right direction?
PrintQueue user control:
<UserControl x:Class="WpfControls.PrintQueue"
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:wpfControls="clr-namespace:WpfControls"
mc:Ignorable="d">
<UserControl.DataContext>
<wpfControls:PrintQueueViewModel/>
</UserControl.DataContext>
<Grid>
<StackPanel Orientation="Horizontal">
<Label Content="Selected Printer:"></Label>
<ComboBox ItemsSource="{Binding Path=PrintQueues, Mode=OneWay}" DisplayMemberPath="Name" SelectedValuePath="Name" Width="200" SelectedValue="{Binding Path=SelectedPrinterName, Mode=TwoWay}"></ComboBox>
</StackPanel>
</Grid>
</UserControl>
Print Queue Codebehind:
public partial class PrintQueue : UserControl
{
public static readonly DependencyProperty CurrentPrinterNameProperty =
DependencyProperty.Register("CurrentPrinterName", typeof (string), typeof (PrintQueue), new PropertyMetadata(default(string)));
public string CurrentPrinterName
{
get { return (DataContext as PrintQueueViewModel).SelectedPrinterName; }
set { (DataContext as PrintQueueViewModel).SelectedPrinterName = value; }
}
public PrintQueue()
{
InitializeComponent();
DataContext = new PrintQueueViewModel();
}
}
PrintQueue View Model:
public class PrintQueueViewModel : ViewModelBase
{
private ObservableCollection<System.Printing.PrintQueue> printQueues;
public ObservableCollection<System.Printing.PrintQueue> PrintQueues
{
get { return printQueues; }
set
{
printQueues = value;
NotifyPropertyChanged(() => PrintQueues);
}
}
private string selectedPrinterName;
public string SelectedPrinterName
{
get { return selectedPrinterName; }
set
{
selectedPrinterName = value;
NotifyPropertyChanged(() => SelectedPrinterName);
}
}
public PrintQueueViewModel()
{
PrintQueues = GetPrintQueues();
}
private static ObservableCollection<System.Printing.PrintQueue> GetPrintQueues()
{
var ps = new PrintServer();
return new ObservableCollection<System.Printing.PrintQueue>(ps.GetPrintQueues(new[]
{
EnumeratedPrintQueueTypes.Local,
EnumeratedPrintQueueTypes.Connections
}));
}
}
Main Window:
<Window x:Class="WPFApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfControls="clr-namespace:WpfControls;assembly=WpfControls" xmlns:wpfApp="clr-namespace:WPFApp"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<wpfApp:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<StackPanel>
<wpfControls:PrintQueue CurrentPrinterName="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.PrinterName, Mode=TwoWay}"></wpfControls:PrintQueue>
</StackPanel>
</Grid>
</Window>
Main Window View Model:
public class MainWindowViewModel : ViewModelBase
{
private string printerName;
public string PrinterName
{
get { return printerName; }
set
{
printerName = value;
NotifyPropertyChanged(() => PrinterName);
}
}
public MainWindowViewModel()
{
PrinterName = "Lexmark T656 PS3";
}
}
Controls in a library need to expose DependencyProperties that you can bind to in your view. Just like WPF's TextBox exposes a Text property.
Your PrintQueue control doesn't expose anything, and instead keeps all its state in a viewmodel that nothing outside can access. Your MainWindowViewModel has no way of getting at the stuff inside PrintQueueViewModel.
You need to expose SelectedPrinterName as a DependencyProperty in the code behind of your PrintQueue xaml. Then in MainWindow.xaml you can bind it to MainWindowViewModel.PrinterName.
If you want to user ViewModels all the way through instead, then MainWindowViewModel should be creating PrintQueueViewModel itself so it can access the properties within.
As per your update / comment:
Unfortunately DependencyProperties don't work like that. The getters/setters aren't even used most of the time, and they should ONLY update the property itself. You're sort of halfway between two worlds at the moment.
If I were in your position, and assuming you can change the library so PrintQueue.xaml doesn't have a hardcoded VM instance in the view, I would just create the PrintQueueViewModel yourself. That's how MVVM is supposed to work:
ViewModel:
public class MainWindowViewModel : ViewModelBase
{
public PrintQueueViewModel PrintQueue { get; private set; }
public MainWindowViewModel()
{
PrintQueue = new PrintQueueViewModel();
PrintQueue.SelectedPrinterName = "Lexmark T656 PS3";
}
}
View:
<Window x:Class="WPFApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfControls="clr-namespace:WpfControls;assembly=WpfControls" xmlns:wpfApp="clr-namespace:WPFApp"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<wpfApp:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<StackPanel>
<wpfControls:PrintQueue DataContext="{Binding PrintQueue}"/>
</StackPanel>
</Grid>
</Window>
Again though, control libraries generally don't have view models, and expose their state via dependency properties since they're designed to be used in XAML.
Component libraries may expose view models, but in that case they wouldn't hard code the view model in the view.
Did you write the library? If not, how did the author expect people to use it?
I think with this small changes everything should work
<ComboBox ItemsSource="{Binding Path=PrintQueues, Mode=OneWay}" DisplayMemberPath="Name" Width="200" SelectedItem="{Binding Path=SelectedPrinter, Mode=TwoWay}"></ComboBox>
private System.Printing.PrintQueue selectedPrinter;
public System.Printing.PrintQueue SelectedPrinter
{
get { return selectedPrinter; }
set
{
selectedPrinter = value;
NotifyPropertyChanged(() => SelectedPrinter);
}
}
Now from the main window you can modify SelectedPrinter on the viewmodel and the change should be reflected on the view
(PrintQueue.DataContext as PrintQueueViewModel).SelectedPrinter = ...
I tried your code and your bindings of the PrintQueueView to the corresponding view model work fine. Your problem is that the MainWindowViewModel does not know about the PrintQueueViewModel and thus cannot retrieve the value of the selected printer when the main window closes (I guess that is the scenario you want to implement).
The quickest solution to your problem would be to do the following steps:
In MainWindow.xaml, give PrintQueue a Name so you can access it in the code behind
In MainWindow.xaml.cs, override the OnClosing method. In it you can retrieve the view model as follows: var viewModel = (PrintQueueViewModel)PrintQueue.DataContext;. After that you can retrieve the selected value and save it or whatever.
In the MainWindow constructor after InitializeComponent, you can retrieve your saved value from a file and set it on the PrintQueueViewModel by retrieving it the same way as in the previous step.
Whole code in MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// Retrieve your selected printer here; in this case, I just set it directly
var selectedPrinter = "Lexmark T656 PS3";
var viewModel = (PrintQueueViewModel)PrintQueue.DataContext;
viewModel.SelectedPrinterName = selectedPrinter;
}
protected override void OnClosing(CancelEventArgs e)
{
var viewModel = (PrintQueueViewModel)PrintQueue.DataContext;
var selectedPrinterName = viewModel.SelectedPrinterName;
// Save the name of the selected printer here
base.OnClosing(e);
}
}
Please remember that the major point of view models is the ability to unit-test GUI logic and to disconnect GUI appearance and logic. Your view models should not be able to retrieve all the possible printers of your system but should obtain these values by e.g. Dependency Injection. I would advise you to read about SOLID programming.

Binding UserControl to its own dependencyProperty doesn't work

Im having a problem where I can't create a User Control which uses properties of an custom object when the parent has set that object to data bind.
To try an explain what I mean here is the code.
Custom object:
public class MyObj
{
public string Text { get; set; }
public MyObj(string text)
{
Text = text;
}
}
User Control Code Behind:
/// <summary>
/// Interaction logic for MyControl.xaml
/// </summary>
public partial class MyControl : UserControl
{
public static readonly DependencyProperty ObjectProperty =
DependencyProperty.Register("Object", typeof (MyObj), typeof (MyControl), new PropertyMetadata(default(MyObj)));
public MyObj Object
{
get { return (MyObj) GetValue(ObjectProperty); }
set { SetValue(ObjectProperty, value); }
}
public MyControl()
{
InitializeComponent();
}
}
User control XAML:
<UserControl x:Class="Test.MyControl"
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"
d:DesignHeight="300" d:DesignWidth="300" DataContext="{Binding RelativeSource={RelativeSource Self}}">
<TextBlock Text="{Binding Object.Text}"/>
So all I expect is for MyControl to display a TextBlock with text showing whatever string is in MyObj.Text;
If I add the control in code, without any bindings, then this works Okay e.g.
MyControl myControl = new MyControl(){ Object = new MyObj("Hello World!") };
grid.Children.Add(myControl);
However if I try to use data binding this doesn't display anything, here is the code for MainWindow.
CodeBehind:
public partial class MainWindow : Window, INotifyPropertyChanged
{
private MyObj _Object;
public MyObj Object
{
get { return _Object; }
set
{
_Object = value;
OnPropertyChanged("Object");
}
}
public MainWindow()
{
InitializeComponent();
Object = new MyObj("HELLO");
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML:
Could anyone point me in the right direction, I guess it's something to do with using relative source binding on the UserControl but I'm not sure.
Thanks
I've personally never used a relative self binding on a UserControl, so I'm unsure if it works. You may try setting the x:Name of your UserControl, and use that in the binding.
<UserControl x:Class="Test.MyControl"
...
x:Name="window">
<TextBlock Text="{Binding ElementName=window, Path=Object.Text}"/>
</UserControl>
Note that if a data-binding fails to bind at runtime, you should also see a related error message in the Output window.
it's been a long time .. but since there is a new technique i would like to post it here.
Compiled Time Binding : this is a new type of binding introduced with windows 10. this binding has a lot of performance benefits classic binding.
And the extra benefit you need not set any DataContext the Page or Control itself is the DataContext you can bind to anything in the page or Control
<UserControl x:Class="Test.MyControl"
...
x:Name="window">
<TextBlock Text="{x:Bind Object.Text}"/>
</UserControl>
But does this work perfectly as you have imagined .. No!! not as u guessed.
and there is an answer to it .
Compiled time binding are by default set to OneTime as opposed to classic bindings that are se to OneWay.
so you need to explicitly set the mode to OneWay to ensure the value always updates.
<UserControl x:Class="Test.MyControl"
...
x:Name="window">
<TextBlock Text="{x:Bind Object.Text,Mode=OneWay}"/>
</UserControl>

Making AvalonEdit MVVM compatible

I'm trying to make Avalon MVVM compatible in my WPF application. From googling, I found out that AvalonEdit is not MVVM friendly and I need to export the state of AvalonEdit by making a class derived from TextEditor then adding the necessary dependency properties. I'm afraid that I'm quite lost in Herr Grunwald's answer here:
If you really need to export the state of the editor using MVVM, then I suggest you create a class deriving from TextEditor which adds the necessary dependency properties and synchronizes them with the actual properties in AvalonEdit.
Does anyone have an example or have good suggestions on how to achieve this?
Herr Grunwald is talking about wrapping the TextEditor properties with dependency properties, so that you can bind to them. The basic idea is like this (using the CaretOffset property for example):
Modified TextEditor class
public class MvvmTextEditor : TextEditor, INotifyPropertyChanged
{
public static DependencyProperty CaretOffsetProperty =
DependencyProperty.Register("CaretOffset", typeof(int), typeof(MvvmTextEditor),
// binding changed callback: set value of underlying property
new PropertyMetadata((obj, args) =>
{
MvvmTextEditor target = (MvvmTextEditor)obj;
target.CaretOffset = (int)args.NewValue;
})
);
public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
public new int CaretOffset
{
get { return base.CaretOffset; }
set { base.CaretOffset = value; }
}
public int Length { get { return base.Text.Length; } }
protected override void OnTextChanged(EventArgs e)
{
RaisePropertyChanged("Length");
base.OnTextChanged(e);
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
Now that the CaretOffset has been wrapped in a DependencyProperty, you can bind it to a property, say Offset in your View Model. For illustration, bind a Slider control's value to the same View Model property Offset, and see that when you move the Slider, the Avalon editor's cursor position gets updated:
Test XAML
<Window x:Class="AvalonDemo.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:avalonExt="clr-namespace:WpfTest.AvalonExt"
DataContext="{Binding RelativeSource={RelativeSource Self},Path=ViewModel}">
<StackPanel>
<avalonExt:MvvmTextEditor Text="Hello World" CaretOffset="{Binding Offset}" x:Name="editor" />
<Slider Minimum="0" Maximum="{Binding ElementName=editor,Path=Length,Mode=OneWay}"
Value="{Binding Offset}" />
<TextBlock Text="{Binding Path=Offset,StringFormat='Caret Position is {0}'}" />
<TextBlock Text="{Binding Path=Length,ElementName=editor,StringFormat='Length is {0}'}" />
</StackPanel>
</Window>
Test Code-behind
namespace AvalonDemo
{
public partial class TestWindow : Window
{
public AvalonTestModel ViewModel { get; set; }
public TestWindow()
{
ViewModel = new AvalonTestModel();
InitializeComponent();
}
}
}
Test View Model
public class AvalonTestModel : INotifyPropertyChanged
{
private int _offset;
public int Offset
{
get { return _offset; }
set
{
_offset = value;
RaisePropertyChanged("Offset");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
You can use the Document property from the editor and bind it to a property of your ViewModel.
Here is the code for the view :
<Window x:Class="AvalonEditIntegration.UI.View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:AvalonEdit="clr-namespace:ICSharpCode.AvalonEdit;assembly=ICSharpCode.AvalonEdit"
Title="Window1"
WindowStartupLocation="CenterScreen"
Width="500"
Height="500">
<DockPanel>
<Button Content="Show code"
Command="{Binding ShowCode}"
Height="50"
DockPanel.Dock="Bottom" />
<AvalonEdit:TextEditor ShowLineNumbers="True"
Document="{Binding Path=Document}"
FontFamily="Consolas"
FontSize="10pt" />
</DockPanel>
</Window>
And the code for the ViewModel :
namespace AvalonEditIntegration.UI
{
using System.Windows;
using System.Windows.Input;
using ICSharpCode.AvalonEdit.Document;
public class ViewModel
{
public ViewModel()
{
ShowCode = new DelegatingCommand(Show);
Document = new TextDocument();
}
public ICommand ShowCode { get; private set; }
public TextDocument Document { get; set; }
private void Show()
{
MessageBox.Show(Document.Text);
}
}
}
source : blog nawrem.reverse
Not sure if this fits your needs, but I found a way to access all the "important" components of the TextEditor on a ViewModel while having it displayed on a View, still exploring the possibilities though.
What I did was instead of instantiating the TextEditor on the View and then binding the many properties that I will need, I created a Content Control and bound its content to a TextEditor instance that I create in the ViewModel.
View:
<ContentControl Content="{Binding AvalonEditor}" />
ViewModel:
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Highlighting;
// ...
private TextEditor m_AvalonEditor = new TextEditor();
public TextEditor AvalonEditor => m_AvalonEditor;
Test code in the ViewModel (works!)
// tests with the main component
m_AvalonEditor.SyntaxHighlighting = HighlightingManager.Instance.GetDefinition("XML");
m_AvalonEditor.ShowLineNumbers = true;
m_AvalonEditor.Load(#"C:\testfile.xml");
// test with Options
m_AvalonEditor.Options.HighlightCurrentLine = true;
// test with Text Area
m_AvalonEditor.TextArea.Opacity = 0.5;
// test with Document
m_AvalonEditor.Document.Text += "bla";
At the moment I am still deciding exactly what I need my application to configure/do with the textEditor but from these tests it seems I can change any property from it while keeping a MVVM approach.

Categories

Resources