Value Update notifications between ViewModels - c#

I have a Tab Control, with an "Exams" tab and a "Templates" tab. I want to share information between the two tabs.
My background is mostly in web with React and Redux, so my instinct is to have the shared information (of model type Exam) belong to the MainWindow, probably as a window resource.
My tech lead, who is much more experienced than me -- in all things, but also in C# though he's also new to WPF), assures me that this is not the way to do it in this setting, that the tabs' ViewModels should be sharing/passing the information.
So I've set up my basic UI with a text box in the Exams tab and a label to display the contents of the text box in the Templates tab.
<Page x:Class="Gui.Tabs.ExamsTab.ExamsHome" Title="ExamsTab">
<Grid VerticalAlignment="Center">
<StackPanel>
<Label Content="EXAMS TAB" />
<TextBox Text="{Binding ExamString, UpdateSourceTrigger=PropertyChanged}"/>
<Label Content="{Binding ExamString}" />
</StackPanel>
</Grid>
</Page>
--------
<Page x:Class="Gui.Tabs.TemplatesTab.TemplatesHome" Title="TemplatesHome">
<Grid VerticalAlignment="Center">
<StackPanel>
<Label Content="TEMPLATES TAB"/>
<Label Content="Text from Exams tab:"/>
<Label BorderBrush="Red" BorderThickness="1" Content="{Binding StringFromExamsTab}"/>
</StackPanel>
</Grid>
</Page>
And I've wired up the ViewModels best I can: (the code-behind and the viewmodels are in the same file for now)
namespace Gui.Tabs.ExamsTab {
public partial class ExamsHome : Page {
public ExamsHome() {
InitializeComponent();
DataContext = ViewModel;
}
public static readonly ExamsTabViewModel ViewModel = new ExamsTabViewModel();
}
public class ExamsTabViewModel :INotifyPropertyChanged {
private string _examString = "Default exam string";
public string ExamString {
get => _examString;
set {
_examString = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "") {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
------
namespace Gui.Tabs.TemplatesTab {
public partial class TemplatesHome : Page {
public TemplatesHome() {
InitializeComponent();
DataContext = viewModel;
}
public static readonly TemplatesTabViewModel viewModel = new TemplatesTabViewModel();
}
public class TemplatesTabViewModel {
public string StringFromExamsTab {
// needs to be notified of value change!
get => ExamsHome.ViewModel.ExamString;
}
}
}
In the Exams tab, the text-box and its corresponding label are synced up.
And, in the Templates tab, the red box says "Default exam string", but it doesn't change with the value in the text-box.
I've read that:
The XAML binding engine listens for [the PropertyChangedEvent] for each bound property on classes that implement the INotifyPropertyChanged interface
Which implies to me that as long as INotifyPropertyChanged is implemented in the Exams VM, the bindings in the Templates tab should be notified.
How can I make this work? Open to the possibility that I'm doing it entirely (or mostly) wrong.

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.

Not getting value when button is clicked

Using data binding to get value from another UserControl when the button is clicked
So, I have a UserControl that is nested in another UserControl that is also nested in the main window, that looks like this,
MainWindow.xaml:
<Window
xmlns:view="clr-namespace:InvoiceAppV2.View"
<Grid>
<TabControl>
<TabItem>
<view:InvoiceControl>
</TabItem>
<TabItem/>
<TabItem/>
</TabControl>
</Grid>
</Window>
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
InvoiceControl xaml:
<UserControl
xmlns:view="clr-namespace:InvoiceAppV2.View"
<Grid>
<view:BuyerInfoControl/>
</Grid>
<Button x:Name="btnSubmit" Content="Submit" Click="BtnSubmit_Click"/>
</UserControl>
InvoiceControl.xaml.cs
public partial class InvoiceControl : UserControl
{
Buyer b = new Buyer();
public InvoiceUserControl()
{
InitializeComponent();
}
private void BtnSubmit_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show(b.BuyerName);
}
}
BuyerInfoControl xaml:
<UserControl
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Label Content="Buyer's Name"/>
<Label Content="Purchase Date"/>
<TextBox x:Name="txtBuyerName" Text="{Binding Path=BuyerName, Mode=TwoWay}"
TextWrapping="NoWrap"/>
<DatePicker x:Name="txtPurchaseDate" Text="{Binding Path=PurchaseDate, Mode=TwoWay}" />
</Grid>
Here's the code to handle the property change
public class Buyer: INotifyPropertyChanged
{
private int _id;
private string _name;
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public int ID
{
get { return _id; }
set { _id = value; }
}
public string BuyerName
{
get { return _name; }
set
{
if(_name != value)
{
_name = value;
PropertyChanged(this, new PropertyChangedEventArgs("BuyerName"));
}
}
}
public Buyer() {}
public Buyer(int id, string name)
{
ID = id;
BuyerName = name;
}
public Buyer(string name)
{
ID = 0;
BuyerName = name;
}
}
xaml.cs
public partial class BuyerInfoControl : UserControl
{
Buyer b;
public BuyerInfoControl()
{
InitializeComponent();
b = new Buyer(txtBuyerName.Text);
this.DataContext = b;
}
}
when a value of "John Doe" is typed in the TextBox and the button is clicked, the buyer's name is null. I usually work in swfit, object-c and MVC. Trying to figure how to play with MVVM and WPF. I have a feeling I'm doing some wrong here.
Here's the design
b = new Buyer(txtBuyerName.Text); only gets the last reference to the state of Text; meaning that it is a snapshot.
To achieve proper binding, it has to occur on the main page where the custom control resides and like the TextBox it will need to bind to the source string. To do that one must provide a Dependency Property on the custom control.
For example, if a dependency property named MyText is added to the control bind like this:
<InvoiceControl MyText="{Binding Path=BuyerName}"/>
<TextBox x:Name="txtBuyerName" Text="{Binding Path=BuyerName, Mode=TwoWay}"
TextWrapping="NoWrap"/>
Since InvoiceControl is a user control, the user control on the main page has to also be bound to the originating BuyerName (as shown) and to do what you want it has to happen via a dependency property put on InvoiceControl.
Wire up a dependency property on InvoiceControl as such:
#region public string MyText
/// <summary>
/// This the dependency property for the control.
/// </summary>
public string MyText
{
get { return GetValue(MyTextProperty) as string; }
set { SetValue(MyTextProperty, value); }
}
/// <summary>
/// Identifies the MyText dependency property.
/// </summary>
public static readonly DependencyProperty MyTextProperty =
DependencyProperty.Register(
"MyText",
typeof(string),
typeof(InvoiceControl),
new PropertyMetadata(string.Empty));
#endregion public string MyText
then load like this
MessageBox.Show(MyText);
See Dependancy Property Overview
Also there may be an issue where you are trying to do the button click, right after typing into the control and the proper change event doesn't fire.
In the TextBox put in the binding UpdateSourceTrigger=PropertyChanged on the so that every text change sends a notification message. That way if one types in and focus moves to the button, the two way binding notification will fire.
Note if you work with custom controls this link has Visual Studio snippets which put in fillable templates for dependency properties in the editor. Ignore the title with Siverlight and opy the snippets to C:\Users\{You}\Documents\Visual Studio 20{XX}\Code Snippets\Visual C#\My Code Snippets, for they will work with all versions of visual studio:
Helpful Silverlight Snippets - Jeff Wilcox

How do you take input of a TextBox and return it in a TextBlock

I am new to MVVM and I am trying to type a string into a textbox and it return on a textblock on another page.
In my Views folder I have this code in xaml which is the textbox that I want to type into:
<TextBox x:Name="date" Text="{Binding Date}" Grid.Row="0" TextAlignment="Right" TextWrapping="Wrap" Margin="0 10 0 1" Padding="1" />
This is a different wpf page that has the textblock and I want what was typed in the textbox to appear here:
<TextBlock Grid.Row="0" TextAlignment="Right" TextWrapping="Wrap" Margin="0 0 0 2" Padding="1" Text="{Binding Date}" />
In my Model folder I have the class Data Entry which looks like this:
public class DataEntry
{
public string Date { get; set; }
}
In my ViewModels folder I have:
namespace FumeHood1._0._0.ViewModels
{
public class MainViewModel : INotifyPropertyChanged
{
public DataEntry DataEntry { get; set; }
private string date;
public string Date
{
get { return date; }
set
{
date = value;
OnPropertyChanged(nameof(Date));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I have been looking everywhere and I cant find the right way to do it. If anyone could help it would be amazing. Just trying to make this MVVM pattern work and make more sense to me.
First, set the DataContext of your view.
public partial class MainWindow : Window
{
public MainWindow() {
InitializeComponent();
DataContext = new MainViewModel();
}
}
Or in xaml : (NOT both at the same time)
<Window>
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
</Window>
And make sure the same instance of MainViewModel is used in both pages.
Second, carefully configure your bindings to act as intended:
<TextBox Text="{Binding Date, UpdateSourceTrigger=PropertyChanged, Mode=OneWayToSource}"/>
<TextBlock Text="{Binding Date}" />
Note that UpdateSourceTrigger=PropertyChanged makes sure that the view model property is updated while the user types. Mode=OneWayToSource only updates the view model property from the TextBox Text property, but not the other way round.

Binding model with multiple properties in UserControl using one DependencyProperty [duplicate]

This question already has answers here:
Issue with DependencyProperty binding
(3 answers)
Closed 4 years ago.
I would like to be able to bind complex model (many properties) to UserControl through DependencyProperty, and if model would be edited in UserControl I would like to see this edited information inside my binded model.
Example application: Model, UserControl (xaml + cs), MainWindow (xaml + cs). I have no ViewModel to simplify idea.
Model:
public class MyModel : INotifyPropertyChanged
{
private string _surname;
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged();
}
}
public string Surname
{
get => _surname;
set
{
_surname = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
MyModelEditor.xaml (inside Grid):
<DockPanel>
<TextBox Text="{Binding MyModel.Name}"/>
<TextBox Text="{Binding MyModel.Surname}"/>
</DockPanel>
Also contains this line in UserControl root element:
DataContext="{Binding RelativeSource={RelativeSource Self}}"
MyModelEditor.xaml.cs:
public partial class MyModelEditor : UserControl
{
public MyModel MyModel
{
get => (MyModel)GetValue(MyModelProperty);
set => SetValue(MyModelProperty, value);
}
public static readonly DependencyProperty MyModelProperty =
DependencyProperty.Register("MyModel", typeof(MyModel), typeof(MyModelEditor), new FrameworkPropertyMetadata(null));
public MyModelEditor()
{
InitializeComponent();
}
}
MainWindow.xaml (inside Grid):
<DockPanel>
<Button DockPanel.Dock="Bottom" Content="Press Me!" Click="ButtonBase_OnClick"/>
<controls:MyModelEditor MyModel="{Binding MyModel}"/>
</DockPanel>
MainWindow.xaml.cs:
public partial class MainWindow : Window, INotifyPropertyChanged
{
private MyModel _myModel;
public MyModel MyModel
{
get => _myModel;
set
{
_myModel = value;
OnPropertyChanged();
}
}
public MainWindow()
{
InitializeComponent();
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
MessageBox.Show(MyModel?.Name);
}
}
My test scenario: type text in textbox, press button.
Current behavior: Message after pressing button is empty.
Expected behavior: Message after pressing button is same like in textbox.
I wold not like to bind to all properties separately, because in future I will have much more then two properties.
Why current approach does not work?
How can I achieve my goal?
You are apparently not using the UserControl instance as Binding source in your UserControl's XAML. One way to do this would be to set the Binding's RelativeSource:
<TextBox Text="{Binding MyModel.Name,
RelativeSource={RelativeSource AncestorType=UserControl}}"/>
However, you don't need a new dependency property at all for this purpose. Just bind the UserControl's DataContext to a MyModel instance, like
<controls:MyModelEditor DataContext="{Binding MyModel}"/>
The Bindings in the UserControl's XAML would automatically work with the MyModel object, like this:
<DockPanel>
<TextBox Text="{Binding Name}"/>
<TextBox Text="{Binding Surname}"/>
</DockPanel>
For both of your TextBox controls, you should define their Binding with a TwoWay mode (ms docs on binding modes). Which, basically, would assure that the data flow is working in both direction (i.e. from the view model into the view and the other way around):
<DockPanel>
<TextBox Text="{Binding MyModel.Name, Mode=TwoWay}"/>
<TextBox Text="{Binding MyModel.Surname, Mode=TwoWay}"/>
</DockPanel>
As a good practice, you should always explicitly define what is the mode of the the Binding (NOTE: by default it's OneWay TwoWay - how to know which is the default?).
Another tip would be to go ahead and use MvvmHelpers nuget (github project), which could spare you the time of implementing INotifyPropertyChanged. Besides, you shouldn't re-invent the wheel
EDIT: Fixes are in your GitHub repo
Two things to note here
You have not instantiated your ViewModel (i.e. MyModel), so it was always null
You don't need to create DependencyPropery every time you want to pass some information to your UserControl. You could simply bind the DataContext itself

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)

Categories