Set properties in child UserControl of a current View with ViewModel (MVVM) - c#

I have a main View called MainContentView.xaml in wich I have set the DataContext to a ViewModel called MainContentViewModel.cs. Here I can update the values that appear in the UI, just as a normal MVVM would work.
It looks like this:
<UserControl x:Class="namespace:MyProject.View.Home.MainContentView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:UserTools="clr-namespace:MyProject.View.Tools"
xmlns:ViewModel="clr-namespace:MyProject.ViewModel.Home"
mc:Ignorable="d" >
<UserControl.DataContext>
<ViewModel:MainContentViewModel />
</UserControl.DataContext>
<Grid x:Name="mainGrid">
<!-- Normal objects -->
<StackPanel>
<Label Text="Im a label" />
<TextBox />
</StackPanel>
<!-- My Custom Object -->
<UserTools:MyCustomUserControl x:Name="myUserControl" />
</Grid>
</UserControl>
And my MainContentViewModel.cs looks like this:
public class MainContentViewModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
private string myLabel = "";
public string MyLabel {
get { return this.myLabel; }
set {
this.myLabel = value;
OnPropertyChangedEvent("MyLabel");
}
}
public MainContentViewModel() {
MyLabel = string.Format("my label text");
}
protected virtual void OnPropertyChangedEvent(string _propertyName) {
var _handler = this.PropertyChanged;
if(_handler != null) { _handler(this, new PropertyChangedEventArgs(_propertyName)); }
}
}
Now I want to set some properties in the MyCustomUserControl.xaml through it's own MyCustomUserControlViewModel.cs.
My MyCustomUserControl.xaml looks like this:
<UserControl x:Class="MyProject.View.Tools.MyCustomUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModel="clr-namespace:MyProject.ViewModel.Tools"
mc:Ignorable="d" >
<UserControl.DataContext>
<ViewModel:MyCustomUserControlViewModel />
</UserControl.DataContext>
<Grid x:Name="mainGrid">
<Label Content="{Binding myCustomLabel}" />
</Grid>
</UserControl>
And my MyCustomUserControlViewModel.cs looks like this:
public class MyCustomUserControlViewModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
private string myCustomLabel = "";
public string MyCustomLabel {
get { return this.myCustomLabel; }
set {
this.myCustomLabel = value;
OnPropertyChangedEvent("MyCustomLabel");
}
}
public MyCustomUserControlViewModel() {
MyCustomLabel = string.Format("my custom label text");
}
protected virtual void OnPropertyChangedEvent(string _propertyName) {
var _handler = this.PropertyChanged;
if(_handler != null) { _handler(this, new PropertyChangedEventArgs(_propertyName)); }
}
}
Now I want to, from the MainContentViewModel.cs, update the MyCustomLabel property on the MyUserCustomUserControl.xaml, and I suppose I have to do it through the MyCustomUserControlViewModel.cs so I can keep the MVVM pattern.
I've tried something like this:
In the MainContentViewModel.cs
private MyCustomUserControlViewModel _viewModel = new MyCustomUserControlViewModel ();
(...)
public MainContentViewModel() {
(...)
_viewModel.SetMyCustomLabel("my new custom label text");
}
And in MyCustomUserControlViewModel.cs
public void SetMyCustomLabel(string _text) {
MyCustomLabel = _text;
}
But this does not work. I'm guessing it's because I'm instantiating another MyCustomUserControlViewModel.cs object.
So, how can I do this?
UPDATED AND WORKING (Thank you Sheridan)
I picked up one of Sheridan's solutions and ended up with this working solution. In my MainContentView.xaml:
(...)
<UserTools:MyCustomUserControl DataContext="{Binding ChildViewModel, Mode=OneWay}" />
In my MainContentViewModel.cs:
private MyCustomUserControlViewModel childViewModel = new MyCustomUserControlViewModel();
public MyCustomUserControlViewModel ChildViewModel {
get { return childViewModel; }
private set { childViewModel = value; OnPropertyChangedEvent("ChildViewModel"); }
}
And then I can do this:
public MainContentViewModel() {
(...)
ChildViewModel.SetMyCustomLabel("my new custom label text");
}

If your parent view model had access to the actual child view model instance that is used (which it probably doesn't because of the way that you instantiate your child view model), then you could have simply done this:
public MainContentViewModel() {
(...)
_viewModel.CustomLabel = "my new custom label text";
}
1.
One solution would be to remove the child view model from the child view to the parent view model and then to display the child view using a ContentControl and a DataTemplate:
<DataTemplate DataType="{x:Type ViewModels:MyCustomUserControlViewModel}">
<Views:MyCustomUserControlView />
</DataTemplate>
...
In MainContentViewModel:
public MyCustomUserControlViewModel ChildViewModel
{
get { return childViewModel; }
private set { childViewModel = value; NotifyPropertyChanged("ChildViewModel"); }
}
...
<Grid x:Name="mainGrid">
<!-- Normal objects -->
<StackPanel>
<Label Text="Im a label" />
<TextBox />
</StackPanel>
<!-- My Custom Object -->
<ContentControl x:Name="myUserControl" Content="{Binding ChildViewModel}" />
</Grid>
Doing this would enable you to call the property simply, as shown in my first example.
2.
One alternative would be to move the property to the parent view model and bind to it directly from the child UserControl:
<Grid x:Name="mainGrid">
<Label Content="{Binding DataContext.myCustomLabel, RelativeSource={RelativeSource
AncestorType={x:Type Views:MainContentView}}}" />
</Grid>
UPDATE >>>
3.
Ok, I've just had another idea... you could add another property into the parent view model and data bind to the child view model like this maybe:
<UserTools:MyCustomUserControl DataContext="{Binding ChildViewModel,
Mode=OneWayToSource}" x:Name="myUserControl" />
This is just a guess, but it might be able to hook onto the child view model like this... then you could do this in the parent view model:
if (ChildViewModel != null) ChildViewModel.CustomLabel = "my new custom label text";
4.
Actually... I've just had another idea. You could do bind from a property in your parent view model through to the UserControl if you add a DependencyProperty to it:
<UserTools:MyCustomUserControl CustomLabel="{Binding CustomLabelInParentViewModel}"
x:Name="myUserControl" />
You could simply remove the property from the child view model in this case.

Related

Updating user control content from a different control

I am new to wpf format, and ran into a problem at the end of my project.
So let's say I have a top bar, with one textbox and a button.
When the user clicks the button, the user control below this bar should update with the search results from the textbox, and it does, except it does not refresh the UI, only the data storage. For simplicity I will post a demo code modelling the issue, with a single string property.
<!-- the main window -->
<Window.DataContext>
<local:CustomerViewModel/>
</Window.DataContext>
<StackPanel Orientation="Horizontal" VerticalAlignment="Top" Grid.Row="0">
<Label Content="content of the textbox: " Margin="10"/>
<TextBox Width="300" Text="{Binding Customer.Name}"/>
<Button Content="Update" Command="{Binding UpdateCommand}"/>
</StackPanel>
<DockPanel Grid.Row="1">
<local:TestControl />
</DockPanel>
user control:
<!-- the user control named TestControl-->
<UserControl.DataContext>
<local:CustomerViewModel />
</UserControl.DataContext>
<DockPanel LastChildFill="True">
<Label DockPanel.Dock="Top" Content="Saved" />
<Label DockPanel.Dock="Bottom" Content="{Binding Info, UpdateSourceTrigger=PropertyChanged}" />
</DockPanel>
datamodel class:
public class Customer : ObservableObject
{
private string mName;
public string Name
{
get => mName;
set
{
if (value != mName)
{
mName = value;
OnPropertyChanged(nameof(Name));
}
}
}
}
viewmodel class:
public class CustomerViewModel : ObservableObject
{
private Customer mCustomer;
public Customer Customer
{
get => mCustomer;
set
{
if (value != mCustomer)
{
mCustomer = value;
OnPropertyChanged(nameof(Customer));
}
}
}
private string mInfo;
public string Info
{
get => mInfo;
set
{
if (value != mInfo)
{
mInfo = value;
OnPropertyChanged(nameof(Info));
}
}
}
private ICommand mUpdateCommand;
public ICommand UpdateCommand
{
get
{
if (mUpdateCommand == null)
{
mUpdateCommand = new RelayCommand(p => SaveChanges());
}
return mUpdateCommand;
}
}
public void SaveChanges()
{
Info = Customer.Name + " was updated";
MessageBox.Show(Info);
}
public CustomerViewModel()
{
mCustomer = new Customer();
mCustomer.Name = "Test";
Info = mCustomer.Name;
}
}
The correct value is displayed in the messagebox, but it does not change in the user control. I am calling the property changed interface, and have tried to invoke the button press with dispatcher.invoke, same issue, am I missing something very obvious here?
Your usercontrol is creating its own personal instance of the viewmodel, and using that for its DataContext. That usercontrol instance then sets Info on itself, not on the CustomerViewModel that the parent window has for its own datacontext.
<UserControl.DataContext>
<local:CustomerViewModel />
</UserControl.DataContext>
Remove those three lines from your usercontrol. Keep the corresponding lines in the window. The usercontrol will then inherit its datacontext from its parent, and they'll both be on the same page.
Those three lines aren't just declaring the type of viewmodel the view uses; they're creating an actual instance of the class and assigning it to DataContext.

WPF Mvvm navigation with parameters

Following this tutorial (among others) and reading questions asked here I've constructed a navigation mechanism that will allow me to pass parameters between my ViewModels:
Object base - every view model inherits from it:
public abstract class ObjectBase : INotifyPropertyChanged
{
//INotifyPropertyChanged members
...
//Navigation handling
public abstract ObjectBase BackLocation { get; }
public abstract event Action<ObjectBase> NavigateTo;
public abstract string ViewHeader { get; }
}
MainViewModel - in charge of navigation:
public class MainViewModel : ObjectBase
{
private ObjectBase _selectedView;
private CommandBase _backCommand;
public MainViewModel()
{
SelectedView = new FirstViewModel();
}
public ObjectBase SelectedView
{
get { return _selectedView; }
set
{
SetProperty(ref _selectedView, value);
//register to the navigation event of the new view
SelectedView.NavigateTo += (target)=> { SelectedView = target; };
}
}
//This command is bound to a Back button on the main view
public CommandBase BackCommand
{
get { return _backCommand ?? (_backCommand = new CommandBase(Back)); }
}
private void Back(object obj)
{
if (SelectedView.BackLocation != null)
{
SelectedView = SelectedView.BackLocation;
}
else
{
Application.Current.Shutdown();
}
}
}
And the main view:
<Window ...
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type vm:FirstViewModel}">
<views:FirstView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SecondViewModel}">
<views:SecondView/>
</DataTemplate>
</Window.Resources>
<ContentPresenter Content="{Binding SelectedView}"/>
</Window>
My problem is: If I set the DataTemplates in the main view like the above it makes each view aware of it's DataContext so if I want to add the DataContext explicitly to a view in order to use intellisense like this:
<UserControl x:Class="Wpf_NavigationTest.Views.FirstView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="clr-namespace:Wpf_NavigationTest.ViewModels">
<!--this causes the view model's constructor to get called again-->
<UserControl.DataContext>
<viewModels:FirstViewModel/>
</UserControl.DataContext>
<Grid>
<TextBlock Text="User control 1" FontSize="40"/>
</Grid>
the View Model's constructor is called twice, losing the parameters passed by the Navigate event.
The problem here is that you are setting the DataContext inside your UserControl, and also in your main view model.
<UserControl.DataContext>
<viewModels:FirstViewModel/>
</UserControl.DataContext>
The code above is instantiating a new FirstViewModel every time this UserControl is created. Therefore when the control gets created by the ContentControl (based on the DataTemplate), it then goes ahead and also creates a new FirstViewModel.
So, the solution here is to remove the UserControl.DataContext declaration in the UserControl, and you can instead set the DataContext of the ContentControl to that of your SelectedView.
<ContentPresenter Content="{Binding SelectedView}"
DataContext="{Binding SelectedView}"/>
In order to use multiple view models to a single view, you can simply add another DataTemplate:
<DataTemplate DataType="{x:Type vm:ThirdViewModel}">
<views:SecondView/>
</DataTemplate>
For Design-Time data (to get the intellisense), you can make use of d:DataContext as explained in this article.
This will require you to set up some view models as static resources, I would recommend creating them in a separate ResourceDictionary.

Data Template not shown

A listbox data template doesn't show and I cannot figure out why.
If I don't use a DataTemplate and copy the contents into the control section itself, it's fine.
I don't do very much binding in XAML, I usually do it all in code. What did I do wrong?
XAML
<UserControl x:Class="Cis.CustomControls.CisArrivalsPanel"
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" Height="296" Width="876">
<UserControl.Resources>
<DataTemplate x:Key="DataTemplate">
<ListBoxItem>
<StackPanel>
<TextBlock Background="Blue" Text="{Binding Path=StationName}" />
<TextBlock Background="Brown" Text="{Binding Path=ArrivalPlatform}" />
</StackPanel>
</ListBoxItem>
</DataTemplate>
</UserControl.Resources>
<Grid>
<StackPanel Orientation="Horizontal">
<ListBox Width="487" Margin="0,66,0,33" ItemTemplate="{StaticResource DataTemplate}">
</ListBox>
</StackPanel>
</Grid>
</UserControl>
CS
public partial class CisArrivalsPanel : UserControl
{
public CisArrivalsPanel()
{
InitializeComponent();
this.DataContext = new ArrivalRowItem();
}
}
Model
public class ArrivalRowItem : INotifyPropertyChanged
{
public ArrivalRowItem()
{
this.StationName = "Lincoln";
this.ArrivalPlatform = "1";
}
private string _stationName;
public string StationName
{
get
{
return _stationName;
}
set
{
_stationName = value;
NotifyPropertyChanged("StationName");
}
}
private string _arrivalPlatform;
public string ArrivalPlatform
{
get
{
return _arrivalPlatform;
}
set
{
_arrivalPlatform = value;
NotifyPropertyChanged("ArrivalPlatform");
}
}
private DateTime _arrivalDateTime;
public DateTime ArrivalDateTime
{
get
{
return _arrivalDateTime;
}
set
{
_arrivalDateTime = value;
NotifyPropertyChanged("ArrivalDateTime");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
You have everything set up, but you don't actually have any data.
ListBox, like other ItemsControls acts against a collection of data, and generates an instance of the template for each item it finds.
Given that you haven't set ItemsSource or populated any collection I can see, you need to create a collection (probably an ObservableCollection) and set the ItemsSource to it via binding. Then add some items to it, and the ListBox will display them!

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

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

Communicating with ViewModel from MainView

I am new to MVVM and still trying to get a grasp on it so let me know if I'm setting this up wrong. What I have is a UserControl with a ListView in it. I populate this ListView with data from the ViewModel then add the control to my MainView. On my MainView I have a button that I want to use to add an item to the ListView. Here is what I have:
Model
public class Item
{
public string Name { get; set; }
public Item(string name)
{
Name = name;
}
}
ViewModel
public class ViewModel : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
private ObservableCollection<Item> _itemCollection;
public ViewModel()
{
ItemCollection = new ObservableCollection<Item>()
{
new Item("One"),
new Item("Two"),
new Item("Three"),
new Item("Four"),
new Item("Five"),
new Item("Six"),
new Item("Seven")
};
}
public ObservableCollection<Item> ItemCollection
{
get
{
return _itemCollection;
}
set
{
_itemCollection = value;
OnPropertyChanged("ItemCollection");
}
}
}
View (XAML)
<UserControl.Resources>
<DataTemplate x:Key="ItemTemplate">
<StackPanel Orientation="Vertical">
<Label Content="{Binding Name}" />
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<UserControl.DataContext>
<local:ViewModel />
</UserControl.DataContext>
<Grid>
<ListView ItemTemplate="{StaticResource ItemTemplate}" ItemsSource="{Binding ItemCollection}">
</ListView>
</Grid>
MainWindow
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.mainContentControl.Content = new ListControl();
}
private void Button_Add(object sender, RoutedEventArgs e)
{
}
}
MainWindow (XAML)
<Grid>
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Button Width="100" Height="30" Content="Add" Click="Button_Add" />
</StackPanel>
<ContentControl x:Name="mainContentControl" />
</DockPanel>
</Grid>
Now, from what I understand, I should be able to just an item to ItemCollection and it will be updated in the view. How do I do this from the Button_Add event?
Again, if I'm doing this all wrong let me know and point me in the right direction. Thanks
You should not interact directly with the controls.
What you need to do is define a Command (a class that implements the ICommand-interface) and define this command on your ViewModel.
Then you bind the Button's command property to this property of the ViewModel. In the ViewModel you can then execute the command and add an item directly to your list (and thus the listview will get updated through the automatic databinding).
This link should provide more information:
http://msdn.microsoft.com/en-us/library/gg405484(v=pandp.40).aspx#sec11

Categories

Resources