Notify Collection changed across viewmodels - c#

I have a scenario that Main-window has content control i switch user control of particular view through it and have View-Models respectively
In one of my User-Control has another user-control in it. Both have a View-Model
<UserControl DataContext="ViewModel1">
<Grid>
<ListView ItemsSource="{Binding TimeSlotCollection}" <!--This Collection is in ViewModel 1-->
">
</ListView>
<UserControl DataContext="ViewModel2">
<DataGrid x:Name="DataGrid"
CanUserAddRows="True"
ItemsSource="{Binding TimeSlotCollection,Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"/> <!--This Collection is in ViewModel 2-->
</Grid>
</UserControl>
I want Notify the View-model 1 Collection when View-model 2 Collection is Changed
MyViewModel 1
private ObservableCollection<AttendanceTimeSlotModel> _timeslotCollection;
public ObservableCollection<AttendanceTimeSlotModel> TimeSlotCollection
{
get { return _timeslotCollection; }
set
{
Set(TimeSlotCollectionPropertyName, ref _timeslotCollection, value);
}
}
public const string EmployeesCollectionPropertyName = "Employees";
private ObservableCollection<EmployeeModel> _employees;
public ObservableCollection<EmployeeModel> Employees
{
get { return _employees; }
set
{
Set(EmployeesCollectionPropertyName, ref _employees, value);
}
}
public const string IsBusyPropertyName = "IsBusy";
private bool _isBusy = false;
public bool IsBusy
{
get
{
return _isBusy;
}
set
{
Set(IsBusyPropertyName, ref _isBusy, value);
}
}
MyViewModel 2
public const string AttendanceTimeSlotCollectionPropertyName = "TimeSlotCollection";
private ObservableCollection<AttendanceTimeSlotModel> _timeSlotCollection = null;
public ObservableCollection<AttendanceTimeSlotModel> TimeSlotCollection
{
get
{
return _timeSlotCollection;
}
set
{
Set(AttendanceTimeSlotCollectionPropertyName, ref _timeSlotCollection, value);
}
}
/// What i trying
public void AddNewTimeSlot(AttendanceTimeSlotModel timeSlot)
{
_designdataService.InsertTimeSlot(timeSlot);
var Vm = SimpleIoc.Default.GetInstance<ViewModels.AssignTimeSlotViewModel>();
RaisePropertyChanged(Vm.TimeSlotCollection);
}
What i'm trying to achieve when my collection inside View-model 2 is updated i want to notify in View-model 1.

When using MVVM, I like to use just one view model to one view. If you simply take your inner UserControl to be part of its parent UserControl, then you'll only need one view model. Try re-arranging your code like this:
<UserControl DataContext="ViewModel1">
<Grid>
<ListView ItemsSource="{Binding TimeSlotCollection}" />
<UserControl DataContext="{Binding TimeSlotCollection}">
...
<!-- Then inside the inner control -->
<DataGrid x:Name="DataGrid" ItemsSource="{Binding}" ... />
...
</UserControl>
</Grid>
</UserControl>
In this way, you just have to manage the one collection in the one view model and most of your problems will just disappear.

Related

Correct use of WPF view model

I'm teaching myself WPF. My window has two combo boxes: one for Categories and one for Subcategories. When the category selection changes, I want the list of subcategories to update to just those that are in the selected category.
I've created a simple view class for both of the combo boxes. My SubcategoryView class' constructor takes a reference to my CategoryView class and attaches an event handler for when the category selection changes.
public class SubcategoryView : INotifyPropertyChanged
{
protected CategoryView CategoryView;
public SubcategoryView(CategoryView categoryView)
{
CategoryView = categoryView;
CategoryView.PropertyChanged += CategoryView_PropertyChanged;
}
private void CategoryView_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "SelectedItem")
{
_itemsSource = null;
}
}
private ObservableCollection<TextValuePair> _itemsSource;
public ObservableCollection<TextValuePair> ItemsSource
{
get
{
if (_itemsSource == null)
{
// Populate _itemsSource
}
return _itemsSource;
}
}
}
I assign my DataContexts like this.
cboCategory.DataContext = new CategoryView();
cboSubcategory.DataContext = new SubcategoryView(cboCategory.DataContext as CategoryView);
The problem is that selecting a new item in my category combo box does not cause the subcategories to repopulate (even though I confirmed my PropertyChanged handler is being called).
What is the correct way to cause the list to repopulate?
Also, I welcome any other comments about this approach. Instead of passing my CategoryView to the constructor, is it better to indicate this declaratively somehow in the XAML?
Here's how we do it in production code.
Each category knows what its subcategories are. If they're coming from a database or a disk file, the database/webservice method/file reader/whatever would return classes just like that, and you'd create the viewmodels to match. The viewmodel understands the structure of the information but knows and cares nothing about the actual content; somebody else is in charge of that.
Note that this is all very declarative: The only loop is the one that fakes up the demo objects. No event handlers, nothing in codebehind except creating the viewmodel and telling it to populate itself with fake data. In real life you do often end up writing event handlers for special cases (drag and drop, for example). There's nothing non-MVVMish about putting view-specific logic in the codebehind; that's what it's there for. But this case is much too trivial for that to be necessary. We have a number of .xaml.cs files that have sat in TFS for years on end exactly as the wizard created them.
The viewmodel properties are a lot of boilerplate. I have snippets (steal them here) to generate those, with the #regions and everything. Other people copy and paste.
Usually you'd put each viewmodel class in a separate file, but this is example code.
It's written for C#6. If you're on an earlier version we can change it to suit, let me know.
Finally, there are cases where it makes more sense to think in terms of having one combobox (or whatever) filtering another large collection of items, rather than navigating a tree. It can make very little sense to do that in this hierarchical format, particularly if the "category":"subcategory" relationship isn't one-to-many.
In that case, we'd have a collection of "categories" and a collection of all "subcategories", both as properties of the main viewmodel. We would then use the "category" selection to filter the "subcategory" collection, usually via a CollectionViewSource. But you could also give the viewmodel a private full list of all "subcategories" paired with a public ReadOnlyObservableCollection called something like FilteredSubCategories, which you'd bind to the second combobox. When the "category" selection changes, you repopulate FilteredSubCategories based on SelectedCategory.
The bottom line is to write viewmodels which reflect the semantics of your data, and then write views that let the user see what he needs to see and do what he needs to do. Viewmodels shouldn't be aware that views exist; they just expose information and commands. It's often handy to be able to write multiple views that display the same viewmodel in different ways or at different levels of detail, so think of the viewmodel as just neutrally exposing any information about itself that anybody might want to use. Usual factoring rules apply: Couple as loosely as possible (but no more loosely), etc.
ComboDemoViewModels.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace ComboDemo.ViewModels
{
public class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] String propName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
#endregion INotifyPropertyChanged
}
public class ComboDemoViewModel : ViewModelBase
{
// In practice this would probably have a public (or maybe protected) setter
// that raised PropertyChanged just like the other properties below.
public ObservableCollection<CategoryViewModel> Categories { get; }
= new ObservableCollection<CategoryViewModel>();
#region SelectedCategory Property
private CategoryViewModel _selectedCategory = default(CategoryViewModel);
public CategoryViewModel SelectedCategory
{
get { return _selectedCategory; }
set
{
if (value != _selectedCategory)
{
_selectedCategory = value;
OnPropertyChanged();
}
}
}
#endregion SelectedCategory Property
public void Populate()
{
#region Fake Data
foreach (var x in Enumerable.Range(0, 5))
{
var ctg = new ViewModels.CategoryViewModel($"Category {x}");
Categories.Add(ctg);
foreach (var y in Enumerable.Range(0, 5))
{
ctg.SubCategories.Add(new ViewModels.SubCategoryViewModel($"Sub-Category {x}/{y}"));
}
}
#endregion Fake Data
}
}
public class CategoryViewModel : ViewModelBase
{
public CategoryViewModel(String name)
{
Name = name;
}
public ObservableCollection<SubCategoryViewModel> SubCategories { get; }
= new ObservableCollection<SubCategoryViewModel>();
#region Name Property
private String _name = default(String);
public String Name
{
get { return _name; }
set
{
if (value != _name)
{
_name = value;
OnPropertyChanged();
}
}
}
#endregion Name Property
// You could put this on the main viewmodel instead if you wanted to, but this way,
// when the user returns to a category, his last selection is still there.
#region SelectedSubCategory Property
private SubCategoryViewModel _selectedSubCategory = default(SubCategoryViewModel);
public SubCategoryViewModel SelectedSubCategory
{
get { return _selectedSubCategory; }
set
{
if (value != _selectedSubCategory)
{
_selectedSubCategory = value;
OnPropertyChanged();
}
}
}
#endregion SelectedSubCategory Property
}
public class SubCategoryViewModel : ViewModelBase
{
public SubCategoryViewModel(String name)
{
Name = name;
}
#region Name Property
private String _name = default(String);
public String Name
{
get { return _name; }
set
{
if (value != _name)
{
_name = value;
OnPropertyChanged();
}
}
}
#endregion Name Property
}
}
MainWindow.xaml
<Window
x:Class="ComboDemo.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:ComboDemo"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel Orientation="Vertical" Margin="4">
<StackPanel Orientation="Horizontal">
<Label>Categories</Label>
<ComboBox
x:Name="CategorySelector"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
DisplayMemberPath="Name"
MinWidth="200"
/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="20,4,4,4">
<Label>Sub-Categories</Label>
<ComboBox
ItemsSource="{Binding SelectedCategory.SubCategories}"
SelectedItem="{Binding SelectedCategory.SelectedSubCategory}"
DisplayMemberPath="Name"
MinWidth="200"
/>
</StackPanel>
</StackPanel>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
namespace ComboDemo
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var vm = new ViewModels.ComboDemoViewModel();
vm.Populate();
DataContext = vm;
}
}
}
Extra Credit
Here's a different version of MainWindow.xaml, which demonstrates how you can show the same viewmodel in two different ways. Notice that when you select a category in one list, that updates SelectedCategory which is then reflected in the other list, and the same is true of SelectedCategory.SelectedSubCategory.
<Window
x:Class="ComboDemo.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:ComboDemo"
xmlns:vm="clr-namespace:ComboDemo.ViewModels"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
>
<Window.Resources>
<DataTemplate x:Key="DataTemplateExample" DataType="{x:Type vm:ComboDemoViewModel}">
<ListBox
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type vm:CategoryViewModel}">
<StackPanel Orientation="Horizontal" Margin="2">
<Label Width="120" Content="{Binding Name}" />
<ComboBox
ItemsSource="{Binding SubCategories}"
SelectedItem="{Binding SelectedSubCategory}"
DisplayMemberPath="Name"
MinWidth="120"
/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</Window.Resources>
<Grid>
<StackPanel Orientation="Vertical" Margin="4">
<StackPanel Orientation="Horizontal">
<Label>Categories</Label>
<ComboBox
x:Name="CategorySelector"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
DisplayMemberPath="Name"
MinWidth="200"
/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="20,4,4,4">
<Label>
<TextBlock Text="{Binding SelectedCategory.Name, StringFormat='Sub-Categories in {0}:', FallbackValue='Sub-Categories:'}"/>
</Label>
<ComboBox
ItemsSource="{Binding SelectedCategory.SubCategories}"
SelectedItem="{Binding SelectedCategory.SelectedSubCategory}"
DisplayMemberPath="Name"
MinWidth="200"
/>
</StackPanel>
<GroupBox Header="Another View of the Same Thing" Margin="4">
<!--
Plain {Binding} just passes along the DataContext, so the
Content of this ContentControl will be the main viewmodel.
-->
<ContentControl
ContentTemplate="{StaticResource DataTemplateExample}"
Content="{Binding}"
/>
</GroupBox>
</StackPanel>
</Grid>
</Window>
Using single view-model in that case is really simpler, as mentioned in comments. For example, I'll use just strings for combo box items.
To demonstrate correct using of view model, we'll track changes of category through binding rather than UI event. So, besides ObservableCollections you'll need SelectedCategory property.
View-model:
public class CommonViewModel : BindableBase
{
private string selectedCategory;
public string SelectedCategory
{
get { return this.selectedCategory; }
set
{
if (this.SetProperty(ref this.selectedCategory, value))
{
if (value.Equals("Category1"))
{
this.SubCategories.Clear();
this.SubCategories.Add("Category1 Sub1");
this.SubCategories.Add("Category1 Sub2");
}
if (value.Equals("Category2"))
{
this.SubCategories.Clear();
this.SubCategories.Add("Category2 Sub1");
this.SubCategories.Add("Category2 Sub2");
}
}
}
}
public ObservableCollection<string> Categories { get; set; } = new ObservableCollection<string> { "Category1", "Category2" };
public ObservableCollection<string> SubCategories { get; set; } = new ObservableCollection<string>();
}
Where SetProperty is implementation of INotifyPropertyChanged.
When you select category, the setter of SelectedCategory property triggers and you can fill subcatagory items depending on selected category value. Do not replace collection object itself! You should clear existing items and then add new ones.
In xaml, besides ItemsSource for both combo boxes, you'll need bind SelectedItem for category combo box.
XAML:
<StackPanel x:Name="Wrapper">
<ComboBox ItemsSource="{Binding Categories}" SelectedItem="{Binding SelectedCategory, Mode=OneWayToSource}" />
<ComboBox ItemsSource="{Binding SubCategories}" />
</StackPanel>
Then just assign view-model to wrapper's data context:
Wrapper.DataContext = new CommonViewModel();
And code for BindableBase:
using System.ComponentModel;
using System.Runtime.CompilerServices;
public abstract class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(storage, value))
{
return false;
}
storage = value;
this.OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

WPF Button Command binding

I have a MainWindow:Window class which holds all the data in my program. The MainWindow's .xaml contains only an empty TabControl which is dynamically filled (in the code-behind).
One of the tabs (OptionsTab) has its .DataContext defined as the MainWindow, granting it access to all of the data. The OptionsTab has a DataGrid which has a column populated with Buttons, as shown below:
The DataGrid is populated with DataGridTemplateColumns, where the DataTemplate is defined in the main <Grid.Resources>. I would like to bind this button to a function in the MainWindow (not the OptionsTab in which it resides).
When the OptionsTab is created, it's .DataContext is set as the MainWindow, so I would have expected that defining the DataTemplate as below would have done it.
<DataTemplate x:Key="DeadLoadIDColumn">
<Button Content="{Binding Phases, Path=DeadLoadID}" Click="{Binding OpenDeadLoadSelector}"/>
</DataTemplate>
I thought this would mean the Click event would be bound to the desired OptionsTab.DataContext = MainWindow's function.This, however, didn't work (the Content did, however). So then I started looking things up and saw this answer to another SO question (by Rachel, who's blog has been of great help for me), from which I understood that you can't {bind} the click event to a method, but must instead bind the Command property to an ICommand property (using the helper RelayCommand class) which throws you into the desired method. So I implemented that, but it didn't work. If I place a debug breakpoint at the DeadClick getter or on OpenDeadLoadSelector() and run the program, clicking on the button doesn't trigger anything, meaning the {Binding} didn't work.
I would like to know if this was a misunderstanding on my part or if I simply did something wrong in my implementation of the code, which follows (unrelated code removed):
MainWindow.xaml
<Window x:Class="WPF.MainWindow"
x:Name="Main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF" SizeToContent="WidthAndHeight">
<TabControl Name="tabControl"
SelectedIndex="1"
ItemsSource="{Binding Tabs, ElementName=Main}">
</TabControl>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
ICommand deadClick;
public ICommand DeadClick
{
get
{
if (null == deadClick)
deadClick = new RelayCommand(p => OpenDeadLoadSelector());
return deadClick;
}
}
public ObservableCollection<TabItem> Tabs = new ObservableCollection<TabItem>();
public static DependencyProperty TabsProperty = DependencyProperty.Register("Tabs", typeof(ICollectionView), typeof(MainWindow));
public ICollectionView ITabsCollection
{
get { return (ICollectionView)GetValue(TabsProperty); }
set { SetValue(TabsProperty, value); }
}
public ObservableCollection<NPhase> Phases = new ObservableCollection<NPhase>();
public static DependencyProperty PhasesProperty = DependencyProperty.Register("Phases", typeof(ICollectionView), typeof(MainWindow));
public ICollectionView IPhasesCollection
{
get { return (ICollectionView)GetValue(PhasesProperty); }
set { SetValue(PhasesProperty, value); }
}
public ObservableCollection<string> Loads = new ObservableCollection<string>();
public static DependencyProperty LoadsProperty = DependencyProperty.Register("Loads", typeof(ICollectionView), typeof(MainWindow));
public ICollectionView ILoadsCollection
{
get { return (ICollectionView)GetValue(LoadsProperty); }
set { SetValue(LoadsProperty, value); }
}
void OpenDeadLoadSelector()
{
int a = 1;
}
public MainWindow()
{
var optionsTab = new TabItem();
optionsTab.Content = new NOptionsTab(this);
optionsTab.Header = (new TextBlock().Text = "Options");
Tabs.Add(optionsTab);
ITabsCollection = CollectionViewSource.GetDefaultView(Tabs);
Loads.Add("AS");
Loads.Add("2");
InitializeComponent();
}
}
OptionsTab.xaml
<UserControl x:Class="WPF.NOptionsTab"
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:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
mc:Ignorable="d"
xmlns:l="clr-namespace:WPF">
<Grid>
<Grid.Resources>
<DataTemplate x:Key="DeadLoadIDColumn">
<Button Content="{Binding Phases, Path=DeadLoadID}" Command="{Binding Path=DeadClick}"/>
</DataTemplate>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<!-- ... -->
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<!-- ... -->
</Grid>
<Grid Grid.Row="1">
<!-- ... -->
</Grid>
<l:NDataGrid Grid.Row="2"
x:Name="PhaseGrid"
AutoGenerateColumns="False">
<l:NDataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Path=Name}"/>
<DataGridTextColumn Header="Date (days)" Binding="{Binding Path=Date}"/>
<DataGridTemplateColumn Header="Deadload" CellTemplate="{StaticResource DeadLoadIDColumn}"/>
</l:NDataGrid.Columns>
</l:NDataGrid>
</Grid>
</UserControl>
OptionsTab.xaml.cs
public NOptionsTab(MainWindow w)
{
DataContext = w;
InitializeComponent();
PhaseGrid.ItemsSource = w.Phases;
}
While we're at it (and this might be a related question), why does {Binding Phases, Path=DeadLoadID} work on the DataTemplate (which is why the buttons appear with "Select"), but if I do {Binding Phases, Path=Name} in the PhaseGrid and remove the .ItemsSource code from the constructor, nothing happens? Shouldn't the PhaseGrid inherit its parent's (NOptionsTab / Grid) DataContext? Hell, even setting PhaseGrid.DataContext = w; doesn't do anything without the .ItemsSource code.
EDIT (27/04/14):
I think that knowing the contents of the NPhase class itself will be of use, so here it is:
public class NPhase : INotifyPropertyChanged
{
string name;
double date;
string deadLoadID = "Select";
public event PropertyChangedEventHandler PropertyChanged;
public string Name
{
get { return name; }
set
{
name = value;
EmitPropertyChanged("Name");
}
}
public double Date
{
get { return date; }
set
{
date = value;
EmitPropertyChanged("Date");
}
}
public string DeadLoadID
{
get { return deadLoadID; }
set
{
deadLoadID = value;
EmitPropertyChanged("DeadLoadID");
}
}
void EmitPropertyChanged(string property)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
public NPhase(double _date, string _name)
{
date = _date;
name = _name;
}
}
EDIT (29/04/14):
A simplified project (getting rid of everything that wasn't necessary) can be downloaded from here (https://dl.dropboxusercontent.com/u/3087637/WPF.zip)
I think that there is the problem that you do not specify data source properly for the data item inside your grid.
I think that the data source for your button column is NPhase instance. So it has no DeadClick property. So, you can check it using Output window in Visual Studio.
I suggest that you can do something like that:
<DataTemplate x:Key="DeadLoadIDColumn">
<Button Content="{Binding Phases, Path=DeadLoadID}"
Command="{Binding Path=DataContext.DeadClick, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type l:NDataGrid}}}"/>
</DataTemplate>
I currently do not understand how you can compile Content="{Binding Phases, Path=DeadLoadID}", because as I thought the default value for Binding clause is the Path property, and you have specified it twice.
EDIT
After I got the small solution all becomes clear. Here is the modified solution. All what I changed in it - I have added RelativeSource to the command binding as I described above, and I added MainWindow as DataContext for your OptionsTab (you have specified it in the question, but not in the project). That's it - all works fine - the command getter is called, and the command is executed when you click the button.

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

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.

WPF Binding a List to a DataGrid

This is my first time working with a WPF datagrid. From what I understand I am supposed to bind the grid to a public propery in my viewmodel. Below is the ViewModel code, as I step through the debugger GridInventory is getting set to List containing 2606 records however these records never show in the datagrid. What am I doing wrong?
public class ShellViewModel : PropertyChangedBase, IShell
{
private List<ComputerRecord> _gridInventory;
public List<ComputerRecord> GridInventory
{
get { return _gridInventory; }
set { _gridInventory = value; }
}
public void Select()
{
var builder = new SqlConnectionBuilder();
using (var db = new DataContext(builder.GetConnectionObject(_serverName, _dbName)))
{
var record = db.GetTable<ComputerRecord>().OrderBy(r => r.ComputerName);
GridInventory = record.ToList();
}
}
}
My XAML is
<Window x:Class="Viewer.Views.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="InventoryViewer" Height="647" Width="1032" WindowStartupLocation="CenterScreen">
<Grid>
<DataGrid x:Name="GridInventory" ItemsSource="{Binding GridInventory}"></DataGrid>
<Button x:Name="Select" Content="Select" Height="40" Margin="600,530,0,0" Width="100" />
</Grid>
</Window>
i think you need call raisepropertychanged event in your GridInventory setter so that view can get notified.
public List<ComputerRecord> GridInventory
{
get { return _gridInventory; }
set
{ _gridInventory = value;
RaisePropertyChanged("GridInventory");
}
}
The page's datacontext is not bound to an instance of the View Model. In the code behind after the InitializeComponent call, assign the datacontext such as:
InitializeComponent();
DataContext = new ShellViewModel();
I think you should use the RaisePropertyChanged in the ViewModel and Model and also should set the DataContext in the View.
<Window.DataContext>
<local:ShellViewModel />
</Window.DataContext>
You might want to consider using bind ObservableCollection to the datagrid. Then you don't need to maintain a private member _gridInventory and a public property GridInventory
//viewModel.cs
public ObservableCollection<ComputerRecord> GridInventory {get; private set;}
//view.xaml
<DataGrid ... ItemsSource="{Binding GridInventory}" .../>

design pattern problem

Hello I have a application design problem and I home you can help me solve it....
This is my first application in silverlight and the first application using mvvm design pattern and I am not sure I am applying mvvm how I am supposed to..
The application is a dynamic application and at runtime I can add/remove usercontrols...
So I have a MainWindowView that has behind a MainWindowModel.
The MainWindowModel has a list of Workspaces witch are in fact WorkspaceModel classes...
I have multiple UserControls and everyone off them has his own view model witch inherits WorkspaceModel.
The Workspaces property is binded to a container in MainWindowView so adding to the Workspaces list a new UserControlModel will automatically add that control to the view.
Now where is my problem... I want to make this dynamically added usercontrols to interact. Lets say one user control is a tree and one is a grid... I want a method to say that Itemsource property of Grid UserControl Model (WorkspaceModel) to be binded to SelectedNode.Nodes Property from the Tree Usercontrol Model (WorkspaceModel).
The MainWindowModel has a property name BindingEntries witch has a list of BindingEntry...
BindingEntry stores the source property and the destination property of the binding like my workspacemodel_1.SelectedNode.Nodes -> workspacemodel_2.ItemSource...
Or as a variation the MainWindowView has a property ViewStateModel. This ViewStateModel class has dynamic created properties - "injected" with property type descriptors/reflections etc... So the user can define at run time the displayed usercontrols (by modifying the Workspaces list) and can define a view model (the ViewStameModel) and the binding is between workspacemodel properties and this ViewStateModel properties...
So I actually want to bind 2 view models one to another... How to do that?
Create an observer pattern?
Is the design until now totally wrong?
I hope it makes sense.....
I will try to add some sample code...the project is quite big I will try co put only the part I have mentioned in the problem desciption... I hope I will not miss any pice of code
First of all
public class MainWindowModel : ModelBase
{
private ObservableCollection<WorkspaceModel> _workspaces;
private ModelBase _userViewModel;
public MainWindowModel()
{
base.DisplayName = "MainWindowModel";
ShowTreeView(1);
ShowTreeView(2);
ShowGridView(3);
ShowGridView(4);
UserViewModel = new ViewModel(); //this is the ViewStateModel
}
void ShowTreeView(int id)
{
WorkspaceModel workspace = ControlFactory.CreateModel("TreeControlModel", id);
this.Workspaces.Add(workspace);
OnPropertyChanged("Workspaces");
SelectedWorkspace = workspace;
}
void ShowGridView(int id)
{
WorkspaceModel workspace = ControlFactory.CreateModel("GridControlModel", id);
this.Workspaces.Add(workspace);
OnPropertyChanged("Workspaces");
SelectedWorkspace = workspace;
}
public ObservableCollection<WorkspaceModel> Workspaces
{
get
{
if (_workspaces == null)
{
_workspaces = new ObservableCollection<WorkspaceModel>();
}
return _workspaces;
}
}
public ModelBase UserViewModel
{
get
{
return _userViewModel;
}
set
{
if (_userViewModel == value)
{
return;
}
_userViewModel = value;
OnPropertyChanged("UserViewModel");
}
}
}
snippets from MainappView
<DataTemplate x:Key="WorkspaceItemTemplate">
<Grid >
//workaround to use Type as in WPF
<Detail:DetailsViewSelector Content="{Binding}" TemplateType="{Binding}" >
<Detail:DetailsViewSelector.Resources>
<DataTemplate x:Key="TreeControlModel" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
<TreeControl:TreeControlView />
</DataTemplate>
<DataTemplate x:Key="GridControlModel" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
<GridControl:GridControlView />
</DataTemplate>
<DataTemplate x:Key="EmptyTemplate">
</DataTemplate>
</Detail:DetailsViewSelector.Resources>
</Detail:DetailsViewSelector>
</Grid>
</DataTemplate>
<DataTemplate x:Key="WorkspacesTemplate">
<ItemsControl IsTabStop="False" ItemsSource="{Binding}" ItemTemplate="{StaticResource WorkspaceItemTemplate}" Margin="6,2"/>
</DataTemplate>
</UserControl.Resources>
<toolkit:HeaderedContentControl Grid.Column="2" HorizontalAlignment="Stretch" Name="hccWorkspaces" VerticalAlignment="Top" Header="Workspaces" Content="{Binding Source={StaticResource vm}, Path=Workspaces}" ContentTemplate="{StaticResource WorkspacesTemplate}"/>
public class ControlFactory
{
public static WorkspaceModel CreateModel(string type, int id)
{
switch (type)
{
case "TreeControlModel": return new TreeControlModel() { Id=id}; break;
case "GridControlModel": return new GridControlModel() { Id = id }; break;
}
return null;
}
}
public class GridControlModel : WorkspaceModel
{
#region Fields
ObservableCollection<TreeItem> _items;
TreeItem _selectedItem;
#endregion // Fields
#region Constructor
public GridControlModel()
{
base.DisplayName = "GridControlModel";
}
#endregion // Constructor
#region Public Interface
public ObservableCollection<TreeItem> Items
{
get
{
return _items;
}
set
{
if (_items == value)
return;
_items = value;
OnPropertyChanged("Items");
}
}
public TreeItem SelectedItem
{
get
{
return _selectedItem;
}
set
{
if (_selectedItem.Equals(value))
{
return;
}
_selectedItem = value;
OnPropertyChanged("SelectedItem");
}
}
#endregion // Public Interface
#region Base Class Overrides
protected override void OnDispose()
{
this.OnDispose();
}
#endregion // Base Class Overrides
}
<Grid x:Name="LayoutRoot" Background="White">
<sdk:DataGrid Grid.Column="2" Grid.Row="1" HorizontalAlignment="Stretch" Name="dataGrid1" VerticalAlignment="Stretch" ItemsSource="{Binding Path=SelectedTreeNode.Children}" IsEnabled="{Binding}" AutoGenerateColumns="False">
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn CanUserReorder="True" CanUserResize="True" CanUserSort="True" Header="Id" Width="Auto" Binding="{Binding Id}" />
<sdk:DataGridTextColumn CanUserReorder="True" CanUserResize="True" CanUserSort="True" Header="Name" Width="Auto" Binding="{Binding Name}"/>
</sdk:DataGrid.Columns>
</sdk:DataGrid>
</Grid>
</UserControl>
For communicating between your view models I would suggest taking a look at the Messenger implementation in MVVM Light as a simple solution.
Alternatively, the Mediator Pattern as described here might be interesting: http://marlongrech.wordpress.com/2008/03/20/more-than-just-mvc-for-wpf/

Categories

Resources