Pass parameter via shell when navigating in UWP application using MVVM - c#

Have started learning UWP development by creating a Windows Template Studio application with Horizontal Navigation project type, MVVM Light design pattern and two blank pages (MainPage and StatsPage).
In the MainPage I have a list of items and I want to show the selected item's statistics in the StatsPage when the user hits the relevant NavigationViewItem. In my wpf apps, I would set up a RelayCommand with command parameter on a button in the Main view, and in the viewmodel, call the command's method with the param/arg to open the Stats view with the correct info. In the created Template Studio app, the NavigationViewItem in the ShellPage calls OnItemInvoked() in its VM which doesn't know about the selection in the MainPage view.
<winui:NavigationView.MenuItems>
<winui:NavigationViewItem x:Uid="Shell_Main" helpers:NavHelper.NavigateTo="LG_Ess.ViewModels.MainViewModel" />
<winui:NavigationViewItem x:Uid="Shell_Stats" helpers:NavHelper.NavigateTo="LG_Ess.ViewModels.StatsViewModel" />
</winui:NavigationView.MenuItems>
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="ItemInvoked">
<ic:InvokeCommandAction Command="{x:Bind ViewModel.ItemInvokedCommand}" />
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
How do I pass the MainPage selected item as a param to StatsPage via the ShellPage's NavigationViewItem? I could probably work it out by adding a button to my MainPage and hiding the Shell navigation, but I'd prefer to do it in the Template Studio style.
Further investigations show that the ShellViewModel seems to be the default DataContext for all pages created by the Template Studio. In my pages I have set the Data Context to the autogenerated View Models by adding:
<Page.DataContext>
<local:{PageName}ViewModel/>
</Page.DataContext>
So, it seems I can either have a single ViewModel and treat the pages as UserControls, or map the DataContext for each page to its own ViewModel, and do some ViewModelLocator dancing to access properties from one VM in another.

So, it seems I can either have a single ViewModel and treat the pages as UserControls, or map the DataContext for each page to its own ViewModel, and do some ViewModelLocator dancing to access properties from one VM in another.
Correct. You could define a 'SelectedStats' relevant property in your MainViewModel and make your ListView's SelectedItem bind to this property and set Mode=TwoWay. Then, on your 'StatsPage', you could use EventTriggerBehavior to bind a 'LoadedCommand' in StatsViewModel like the 'ShellPage'. In StatsViewModel, you could get the selected item by calling ViewModelLocator.Current.MainViewModel.SelectedStats.
See my simple code sample:
<!--MainPage.xaml-->
<ListView ItemsSource="{x:Bind ViewModel.list}" SelectedItem="{x:Bind ViewModel.SelectedStats,Mode=TwoWay}">
</ListView>
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
list = new ObservableCollection<string>();
list.Add("string1");
list.Add("string2");
list.Add("string3");
}
public ObservableCollection<string> list { get; set; }
private object _SelectedStats;
public object SelectedStats
{
get { return _SelectedStats; }
set
{
if (_SelectedStats != value)
{
_SelectedStats = value;
RaisePropertyChanged("SelectedStats");
}
}
}
}
<!--StatsPage.xaml-->
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="Loaded">
<ic:InvokeCommandAction Command="{x:Bind ViewModel.LoadedCommand}" />
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
public class StatsViewModel : ViewModelBase
{
public StatsViewModel()
{
}
private ICommand _LoadedCommand;
public ICommand LoadedCommand => _LoadedCommand ?? (_LoadedCommand = new RelayCommand(LoaedAsync));
private async void LoaedAsync()
{
var selectedObject = ViewModelLocator.Current.MainViewModel.SelectedStats;
//TODO:...
await Task.CompletedTask;
}
}

Related

WPF Main menu for different views / viewmodels

I'd like to create an app, containing the main menu (ribbonmenu) and different usercontrols, each assigned to an own ViewModel.
I was told to not implement classic events in code-behind but to use commands. So far, everything fine, commands for needed methods are implemented.
In my previous approach I "loaded" the UserControl, by assigning the corresponding ViewModel to a ContentControl, that loaded the UserControl, that was assigned to the ViewModel in MainWindow.Resource.
My last approach, simplified with a button instead of a menu:
<Window.Resources>
<DataTemplate x:Name="settingsViewTemplate" DataType="{x:Type viewmodels:SettingsViewModel}">
<views:SettingsView DataContext="{Binding SettingsVM, Source={StaticResource Locator}}"/>
</DataTemplate>
<DataTemplate x:Name="projectsViewTemplate" DataType="{x:Type viewmodels:ProjectViewModel}">
<views:ProjectView DataContext="{Binding ProjectVM, Source={StaticResource Locator}}"/>
</DataTemplate>
</Window.Resources>
<StackPanel>
<Button Content="Load Settings" Height="20" Margin="20 20 20 0" Click="ShowSettings"/>
<ContentControl Margin="5" Height="100" Content="{Binding}"/>
</StackPanel>
simplified code-behind:
public SettingsViewModel settingsViewModel;
public MainWindow()
{
InitializeComponent();
settingsViewModel = new SettingsViewModel();
}
private void ShowSettings(object sender, RoutedEventArgs e)
{
DataContext = settingsViewModel;
}
How can I load a UserControl, using ViewModel commands?
Don't use code-behind to handle view models. A View model should handle view models. Generally the same view model that implements the commands.
First create a main view model for the MainWindow as data source. This view model will also handle the switching between the views. It's recommended to let all page view models implement a common base type e.g. IPage.
Also you don't need any locator for this scenario. The views inside the DataTemplate will automatically have their DataContext set to the data type that maps to the DataTemplate. SettingsView will automatically have SetingsViewModel as the DataContext. If this would be the wrong context, then your model design is wrong.
IPage.cs
interface IPage : INotifyPropertyChanged
{
string PageTitel { get; set; }
}
SettingsViewModel.cs
class SettingsViewModel : IPage
{
...
}
ProjectViewModel.cs
class ProjectViewModel : IPage
{
...
}
PageName.cs
public enum PageName
{
Undefined = 0, SettingsPage, ProjectPage
}
MainViewModel.cs
An implementation of RelayCommand can be found at
Microsoft Docs: Patterns - WPF Apps With The Model-View-ViewModel Design Pattern - Relaying Command Logic
class MainViewModel : INotifyPropertyChanged
{
public ICommand SelectPageCommand => new RelayCommand(SelectPage);
public Dictionary<PageName, IPage> Pages { get; }
private IPage selectedPage;
public IPage SelectedPage
{
get => this.selectedPage;
set
{
this.selectedPage = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
this.Pages = new Dictionary<PageName, IPage>
{
{ PageName.SettingsPage, new SettingsViewModel() },
{ PageName.ProjectPage, new ProjectViewModel() }
};
this.SelectedPage = this.Pages.First().Value;
}
public void SelectPage(object param)
{
if (param is PageName pageName
&& this.Pages.TryGetValue(pageName, out IPage selectedPage))
{
this.SelectedPage = selectedPage;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<Window.Resources>
<DataTemplate x:Name="settingsViewTemplate" DataType="{x:Type viewmodels:SettingsViewModel}">
<views:SettingsView />
</DataTemplate>
<DataTemplate x:Name="projectsViewTemplate" DataType="{x:Type viewmodels:ProjectViewModel}">
<views:ProjectView />
</DataTemplate>
</Window.Resources>
<StackPanel>
<!-- Content navigation -->
<StackPanel Orientation="Horizontal">
<Button Content="Load Settings"
Command="{Binding SelectPageCommand}"
CommandParameter="{x:Static PageName.SettingsPage}" />
<Button Content="Load Projects"
Command="{Binding SelectPageCommand}"
CommandParameter="{x:Static PageName.ProjectPage}" />
</StackPanel>
<ContentControl Content="{Binding SelectedPage}" />
<StackPanel>
</Window>
The short version:
public class MyViewModel : ViewModel
public MyViewModel()
{
View = new MyUserControlView();
View.DataContext = this; // allow the view to bind to the viewModel.
}
....
public UIElement View {
get; private set;
}
}
And then in XAML:
<ContentControl Content={Binding View} />
There are variations on this theme but that's the basic premise. e.g., if you have a ViewModel that can be bound to multiple views, or ViewModels that have lifetimes longer than their view, you can use a FrameViewModel class like this:
public class FrameViewModel : INotifyProperyChanged; {
public FrameViewModel(IViewModel viewModel; )
{
ViewModel = viewModel;
View = viewModel.CreateView();
View.DataContext = ViewModel;
}
public IViewModel ViewModel { get; set;...}
public UIElement View { get; set; }
}
And then bind THAT into the host XAML with a ContentControl binding to Frame.View.
A more pure approach is to the use the DataTemplateSelector class to instantiate the User Control in a DataTemplate. This is probably the method that WPF designers had in mind for connecting View and ViewModel in WPF. But it ends up spreading the mapping of View and ViewModel across three separate files (the custom C# DataTemplateSelector implementation; widely-separated static resource declaration and ContentControl wrapper in the hosting Window/Page; and the DataTemplate resources themselves which end up in resource files eventually if you have anything but a trivial number of ViewModel/View bindings.
Purists would argue, I suppose, that there's something dirty about having a viewmodel create a view. But there's something far more dirty about code to make DataTemplateSelectors work spread across five files, and inevitable complications with databindings that ensue while trying to tunnel a binding through a DataTemplate.

How to use data created in one ViewModel on another Page/Viewmodel?

I'm currently building the UI for the app I'm working on and I have a few problems with the bindings.
The Scenario:
I have a pivot control with every pivot element consisting of an extra Frame/Page.
Now I have a TextBlock on the first PivotItem. I bind this to a "string" and use a button to switch between two possible contents of the button.
When the button is on the same Page/Frame it works like a charm. But when I implement a button on the MainPage and implement the same Viewmodel for the MainPage then it doesn't work. It will only change the string content on the MainPage.
Is it possible to implement the change for every Page/Frame?
And when that is done I have a Page where I gather data with a serial port.
I save the data to a List and I want to be able to use this list from 2 different Pages/Frames.
Thinking about the scenarion above then it would probably gather the data for the page where I have the button to get the data but it would probably display nothing on the other page.
How can I build it like I want it to be?
Here is a short example:
Mainpage.xaml
<StackPanel>
<Button Height="50" Width="200" Content="Change" FontSize="30" FontWeight="Bold" Margin="50 50 0 0" Click="{x:Bind MainViewModel.Change}"/>
<Pivot x:Name="MainPivot" Margin="50 50">
<PivotItem Header="Page 1">
<Frame x:Name="Page1" />
</PivotItem>
</Pivot>
</StackPanel>
Mainpage.xaml.cs
public MainPage()
{
this.InitializeComponent();
Page1.Navigate(typeof(Page1));
ViewModel = new MainViewModel();
}
public MainViewModel ViewModel { get; private set; }
Page1.xaml
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="{x:Bind Path=ViewModel.StringModel.String1, Mode=TwoWay}" FontSize="50" FontWeight="Bold" />
<Button Content="Change" FontSize="30" FontWeight="Bold" Click="{x:Bind ViewModel.Change}"/>
</StackPanel>
Page1.xaml.cs
public Page1()
{
this.InitializeComponent();
ViewModel = new MainViewModel();
}
public MainViewModel ViewModel { get; private set; }
MainViewModel.cs
private StringModel _stringModel = new StringModel();
public StringModel StringModel
{
get => _stringModel;
set
{
if (_stringModel != value)
{
_stringModel = value;
OnPropertyChanged();
}
}
}
public void Change()
{
if (StringModel.String1 == "Text1")
{
StringModel.String1 = "Text2";
}
else
{
StringModel.String1 = "Text1";
}
}
StringModel.cs
private string _string1 = "XXX";
public string String1
{
get => _string1;
set
{
_string1 = value;
OnPropertyChanged();
}
}
Sounds like you are missing a "service layer" or "business layer" of your application. You need an external class which manages the data, and can provide models to populate your ViewModels:
I'd suggest using some kind of dependency injection, so each of your page view models have a reference to the DataProvider service class. This class does the serial port work to get a list of models, and provides an interface for getting data and pushing any updates to the ViewModels.
A good way of handling events that are shared, like say a "load data" button that may appear on different view models is an Event Aggregator. A service that can be injected into classes where events can be raised or subscribed to across the application.
Generally children of a XAML parent inherit the binding context of said parent.
So not sure you need to hook up a VM to your frame.
But suppose it does not work with Frames, you are creating a new MainViewModel for the frame as for the mainpage!
The solution here would be to create a singleton MainViewModel and get a hold of that one to hook up the BindingContext.
You can use Publisher-Subscriber pattern (Pub-Sub).
Already explained this here: Communication Between Views in MVVM (Pub-Sub Pattern)

Can I bind a RoutedCommand to a Command in WPF?

I have a ViewModel of the form:
class VM : DependencyObject
{
// This class exposes a command to data binding to delete things from the model
public static DependencyProperty DeleteProperty = DependencyProperty.Register(/*...*/);
public static ICommand Delete {
get {
return (ICommand)this.GetValue(DeleteProperty);
}
set {
this.SetValue(DeleteProperty, value);
}
}
}
For a ListBoxItem I would like to data bind execution of ApplicationCommands.Delete to this ICommand exposed by this ViewModel. That way, when someone presses the menu item that raises ApplicationCommands.Delete, this particular delete will be chosen if focus is currently on this thing:
<!-- Elsewhere... -->
<MenuItem Text="Delete" Command="ApplicationCommands.Delete" />
<!-- Some data template with DataContext == the above DependencyObject -->
<Grid>
<Grid.CommandBindings>
<!--
I want ApplicationCommands.Delete to forward to the command {Binding Delete}
-->
<CommandBinding Command="ApplicationCommands.Delete"
???
/>
</Grid.CommandBindings>
</Grid>
... but CommandBinding can only bind to event handlers; not other commands, so I can't use MVVM-style data binding to attach it. Is there some MVVM mechanism I can use here or am I forced into adding code-behind for the CommandBinding?
You can use this tutorial to bind ApplicationCommands.
1.Add command binding collection property in your view model:
public CommandBindingCollection CommandBindings { get; }
public YourViewModel()
{
//Create a command binding for the delete command
var deleteBinding = new CommandBinding(ApplicationCommands.Delete, DeleteExecuted, DeleteCanExecute);
//Register the binding to the class
CommandManager.RegisterClassCommandBinding(typeof(YourViewModel), DeleteBinding);
//Adds the binding to the CommandBindingCollection
CommandBindings.Add(deleteBinding);
}
Create attached property as mentioned in the tutorial.
Then bind attached property in your UI.

link command of KeyBinding in mainwindow's view model to an user control's view model commandbindings

I have a mainwindow inside which there is a usercontrol which contains a listview.
User control also has a button which copies all the contents of listview to clipboard.
This is how the copy functionality has been implemented.
below is a part of xaml of usercontrol -
<Button Command="Copy"
CommandTarget="{Binding ElementName=testCodeView}"
CommandParameter="Copy"
</Button>
<ListView x:Name="testCodeView"
ItemsSource="{Binding Products}" BorderThickness="0" Grid.Row="1"
ItemTemplate="{StaticResource testViewTemplate}"
ItemContainerStyle="{StaticResource testCodesListItem}"
infra:AttachedProperties.CommandBindings ="{Binding CommandBindings}">
</ListView>
The AttachedProperties class holds the Dependency property "CommandBindings" - below is the code -
public class AttachedProperties
{
public static DependencyProperty CommandBindingsProperty =
DependencyProperty.RegisterAttached("CommandBindings", typeof(CommandBindingCollection), typeof(AttachedProperties),
new PropertyMetadata(null, OnCommandBindingsChanged));
public static void SetCommandBindings(UIElement element, CommandBindingCollection value)
{
if (element != null)
element.SetValue(CommandBindingsProperty, value);
}
public static CommandBindingCollection GetCommandBindings(UIElement element)
{
return (element != null ? (CommandBindingCollection)element.GetValue (CommandBindingsProperty) : null);
}
}
Below is the usercontrol viewmodel's code related to copying the items of listview.
public class UserControlViewModel : INotifyPropertyChanged
{
public CommandBindingCollection CommandBindings
{
get
{
if (commandBindings_ == null)
{
commandBindings_ = new CommandBindingCollection();
}
return commandBindings_;
}
}
public UserControlViewModel
{
CommandBinding copyBinding = new CommandBinding(ApplicationCommands.Copy,
this.CtrlCCopyCmdExecuted, this.CtrlCCopyCmdCanExecute);
// Register binding to class
CommandManager.RegisterClassCommandBinding(typeof(UserControlViewModel), copyBinding);
this.CommandBindings.Add(copyBinding);
}
private void CtrlCCopyCmdExecuted(object sender, ExecutedRoutedEventArgs e)
{
copyToclipboard_.CtrlCCopyCmdExecuted(sender, e);
}
}
The sender object in CtrlCCopyCmdExecuted function is the listview in usercontrol which is futher used in
copying its contents.
The copy all functionality works fine with the button on user control.
I have to create a key binding for copy functionality in mainwindow. I have created other keybindings in mainwindow which works fine as the commands are defined in MainWindowViewModel, but since the commandbindings of the copy all command are in the usercontrol's view model, I am facing problems in linking command of keybinding in mainwindowviewmodel with the commandbinding in usercontrolviewmodel.
Can Someone help me out with this.
Thanks in advance.
If your parent view model has access to the child view model then you have two main options. Both options involve creating an ICommand property to Bind to in the parent view model:
<KeyBinding Gesture="CTRL+C" Command="{Binding Copy, Mode=OneWay}" />
The first option involves creating a BaseViewModel class that has an abstract ICommand property in it... any class that extends the BaseViewModel class has to implement this command... then you could have the child view model property of this type and simply pass the value along:
public override ICommand Copy
{
get { return new ActionCommand(action => ViewModel.Copy.Execute(null),
canExecute => ViewModel.Copy != null); }
}
I doubt that you want that command on every view model though, so let's investigate option two. The basic idea is that if you have access to the child view model, you can 'pass' the command along to the child:
public ICommand Copy
{
get { return new ActionCommand(action => childViewModel.Copy.Execute(action),
canExecute => childViewModel.Copy.Execute(canExecute)); }
}
ActionCommand is a custom class that is based on the common RelayCommand.
UPDATE >>>
Ok, so I've never used CommandBindings before because it's so much easier using my ActionCommand, or RelayCommand, but I've just investigated it on MSDN and two ideas spring to mind.
First, if you're using the ApplicationCommands.Copy command object in the child UserControl, then surely you can just use that same object in the KeyBinding in the parent view. ApplicationCommands is a static class and Copy is a static property, so wherever you call it, it's the same object:
<KeyBinding Gesture="CTRL+C" Command="ApplicationCommands.Copy" />
Failing that, you could create an ICommand property in your parent view model and either create your own Copy command, or just return the ApplicationCommands.Copy command. Then you could Bind to it from the parent view and the child view... or, you could even add it into the child view CommandBinding object from the parent view model.

Create new ViewB + ViewmodelB in other ViewModelA and show them in ViewA

I use MVVM. I have a ViewA with a Grid in xaml and a ViewModelA. In the ViewModelA I have a Method that looks like this which is called on a Button click:
public void ButtonClickMethod()
{
ViewB viewB = new ViewB();
viewB.DataContext = new ViewBViewModel();
}
How do I add all the created viewBs into the Grid on my ViewA, so I can see them there? Or is my solution wrong in general maybe?
EDIT :
I now used a ObservableCollection<ViewB> in my ViewModelA which notifies on changes in my ViewA xaml at
<ItemsControl ItemsSource="{Binding ObservableCollection<ViewB>}" />
The only problem is the ViewBs shown in the ItemControl should be dragable. Therefore I used expression blend's
Interaction.GetBehaviors(ViewB).Add(new MouseDragElementBehavior() { });
when i create a new ViewB. But it doesn't work.
EDIT2 :
I tried this solution but it doesn't work for me :( Using MouseDragElementBehavior with an ItemsControl and Canvas
Add your Grid an ContentControl and bind it content to your second view.
Here an example with an UserControl.
<ContentControl Grid.Column="0" Margin="5" Content="{Binding SecondView}"/>
Code im ViewModel:
private UserControl secondView;
public UserControl SecondView
{
get
{
return secondView;
}
set
{
this.SetProperty(ref secondView,value);
}
}

Categories