I made a Page MainPage
and a UserControl Pager
. Both have their ViewModel. In Pager
, there are three dependency properties Rows
, Columns
, Source
. I want to pass these properties from Pager
's View to Pager
's ViewModel. I tried this on View's code behind. But it doesn't work...the set
property in PagerViewModel
never be called on debugging. Please, help me...
Here is detail mechanism:
MainPageViewModel
↓Pass the values with binding
MainPage
↓Set tht properties with values from MainPagerViewModel
Pager
(code behind)
↓Bind the properties to PagerViewModel <--- This part is PROBLEM!!!
PagerViewModel
↓Pass the values with binding
Pager
(XAML)
and here is source
[MainPageViewModel.cs]
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using Client.Model;
namespace Client.ViewModel
{
public class MainPageViewModel : ViewModelBase
{
...
public ObservableCollection<IPagableEntry> PagerTableCategoriesItems { get { return TableCategoryRepository.Instance.TableCategories; } }
public int PagerTableCategoriesRows { get { return 1; } }
public int PagerTableCategoriesColumns { get { return 3; } }
...
}
}
[MainPage.xaml]
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:view="clr-namespace:Client.View"
xmlns:viewModel="clr-namespace:Client.ViewModel"
xmlns:resStr="clr-namespace:Client.CommonResources.String"
x:Class="Client.View.MainPage"
Style="{StaticResource common}">
<Page.DataContext>
<viewModel:MainPageViewModel />
</Page.DataContext>
...
<view:Pager x:Name="pagerTableCategories"
Source="{Binding Path=PagerTableCategoriesItems}"
Rows="{Binding Path=PagerTableCategoriesRows}"
Columns="{Binding Path=PagerTableCategoriesColumns}">
</view:Pager>
...
</Page>
[Pager.xaml.cs]
using System;
using System.Collections;
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;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using Client.Model;
using Client.ViewModel;
namespace Client.View
{
public partial class Pager
{
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(ObservableCollection<IPagableEntry>), typeof(Pager), new PropertyMetadata(null, OnSourceChanged));
public static readonly DependencyProperty RowsProperty = DependencyProperty.Register("Rows", typeof(int), typeof(Pager), new PropertyMetadata(1, OnRowsChanged));
public static readonly DependencyProperty ColumnsProperty = DependencyProperty.Register("Columns", typeof(int), typeof(Pager), new PropertyMetadata(1, OnColumnsChanged));
public static readonly DependencyProperty SelectedEntryProperty = DependencyProperty.Register("SelectedEntry", typeof(object), typeof(Pager), new PropertyMetadata(null, OnSelectedEntryChanged));
public int Rows
{
get { return (int)GetValue(RowsProperty); }
set { SetValue(RowsProperty, value); }
}
public int Columns
{
get { return (int)GetValue(ColumnsProperty); }
set { SetValue(ColumnsProperty, value); }
}
public object SelectedEntry
{
get { return GetValue(SelectedEntryProperty); }
set { SetValue(SelectedEntryProperty, value); }
}
public ObservableCollection<IPagableEntry> Source
{
get { return (ObservableCollection<IPagableEntry>)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public Pager()
{
InitializeComponent();
// I want to bind the three custom properties(Rows, Columns, Source) to PagerViewModel's Rows, Columns, Collection
Binding bindingRows = new Binding("Rows");
bindingRows.Mode = BindingMode.TwoWay;
bindingRows.Source = gridPager.DataContext;
gridPager.SetBinding(RowsProperty, bindingRows);
Binding bindingColumns = new Binding("Columns");
bindingColumns.Mode = BindingMode.TwoWay;
bindingColumns.Source = gridPager.DataContext;
gridPager.SetBinding(ColumnsProperty, bindingColumns);
Binding bindingSource = new Binding("Collection");
bindingSource.Mode = BindingMode.TwoWay;
bindingSource.Source = gridPager.DataContext;
gridPager.SetBinding(SourceProperty, bindingSource);
}
private void ListBoxEntriesOnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectedEntry = (sender as ListBox).SelectedItem;
}
private static void OnSelectedEntryChanged(DependencyObject pager, DependencyPropertyChangedEventArgs e)
{
(pager as Pager).SelectedEntry = e.NewValue;
}
private static void OnSourceChanged(DependencyObject pager, DependencyPropertyChangedEventArgs e)
{
(pager as Pager).Source = (ObservableCollection<IPagableEntry>)e.NewValue;
}
private static void OnRowsChanged(DependencyObject pager, DependencyPropertyChangedEventArgs e)
{
(pager as Pager).Rows = (int)e.NewValue;
}
private static void OnColumnsChanged(DependencyObject pager, DependencyPropertyChangedEventArgs e)
{
(pager as Pager).Columns = (int)e.NewValue;
}
}
}
[Pager.xaml]
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:view="clr-namespace:Client.View"
xmlns:viewModel="clr-namespace:Client.ViewModel"
xmlns:resStr="clr-namespace:Client.CommonResources.String"
x:Class="Client.View.Pager">
<Grid x:Name="gridPager">
<Grid.DataContext>
<viewModel:PagerViewModel />
</Grid.DataContext>
...
<ListBox x:Name="listBoxEntries"
ItemsSource="{Binding Path=Collection}"
BorderThickness="0"
Margin="0"
Style="{StaticResource common}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
ItemTemplate="{StaticResource templateTableCategory}"
SelectedItem="{Binding Path=SelectedEntry, Mode=TwoWay}"
SelectionChanged="ListBoxEntriesOnSelectionChanged">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="{Binding Path=Rows}"
Columns="{Binding Path=Columns}"
IsItemsHost="True"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
...
</Grid>
</UserControl>
[PagerViewModel.cs]
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Data;
using System.Windows.Media;
using Client.Model;
namespace Client.ViewModel
{
public class PagerViewModel : ViewModelBase
{
...
ListCollectionView _listCollectionView;
ObservableCollection<IPagableEntry> _collection;
int _rows;
int _columns;
public int Rows
{
get { return _rows; }
set
{
_rows = value;
OnPropertyChanged();
}
}
public int Columns
{
get { return _columns; }
set
{
_columns = value;
OnPropertyChanged();
}
}
public ListCollectionView ListCollectionView
{
get { return _listCollectionView; }
set
{
_listCollectionView = value;
OnPropertyChanged();
}
}
public ObservableCollection<IPagableEntry> Collection
{
get
{
return _collection;
}
set
{
_collection = value;
OnPropertyChanged();
}
}
...
}
}
This solution assumes :
What this code does :
How it does it ?
Get MainViewModel in UC via it's own DataContext. As UC DataContext gets its value from parentwin dctx automatically. But it can happen that for example, your UC is present in some Grid of MainWin and this Grid is using some other viewmodel. In that case you have to use some VisualTree traversal helper methods to reach the root window/page to get it's datacontext.
https://www.dropbox.com/s/5ryc9ndxdu2m6a4/WpfApplication3.rar?dl=0
If you don't want your UC to depend upon your parent, but instead parent using your UC, then from parent you can always access UC easily and do what you want.
Your main confusion comes due to the fact that
Pager
is not a view, but aUserControl
.They both can inherit from
UserControl
class, but the difference is: In MVVM a View (or a Subview akaDataTemplate
) has a ViewModel bound to it (viaDataContext
) or to it's parent, but no "functionality" as in "Code Behind".A
UserControl
on the other side never has a ViewModel that belong to it (read: the logic isn't split into the ViewModel), because aUserControl
is meant to be reused across multiple applications, a View is specific to a ViewModel and only to be used within your application.In a
UserControl
it's perfectly valid to have code behind (for Dependency Properties, which other Application ViewModels can bind to or login inside the code behind). TheUserControl
will expose the DPs for external data-binding. In aView
this is an absolute no-go and violates MVVM pattern.It's a very common trap developers new to MVVM run into and attempt to create ViewModels for a control and find themselves stuck there.
That being said, since (imho, from your examples above I don't see anything app-specific functionality in your Pager) your
Pager
is aUserControl
it doesn't require thePagerViewModel
and it's code should be moved in thePager
's Code Behind.On a side-note It should have been obvious from your Pager's ViewModel class, that your attempt is validating MVVM as it keeps strong references to the View (not only to View class but to ANY WPF related classes!!!).
Enforcing MVVM is much easier if you create separate assemblies.
This is very helpful as if you try to use a Type from Presentation.dll in your Model or ViewModel, it will fail because there is no Reference to the assembly and you instantly know: "Wow! Stop here. I am doing something wrong, it violates MVVM!"
The bold underscored assemblies must be able to run and compile on other platforms (Web, Desktop, WinPhone, Silverlight), so they are not allowed to have this references to view specific assemblies. The Datalayer can differ from platform (i.e. WinPhone apps may want to use SQLite rather than MSSQL, ASP.NET Websites on Linux may prefer MySQL to MSSQL etc.)
As I can understand the problem is the creating a synchronization mechanism between two viewmodels. I'm completely agry with Peter's theory, but I suggest you the next synchronization solution. To solve this problem I'd like to advice you to use a model level syncronization. Just put your required details into a light model class and inject this small model into desired viewmodels. Here is the scheme:
regards,
There are two obvious problems with the code you posted:
OnXXXChanged()
handlers don't do anything. They are responding to changes in the properties that they in turn attempt to set. I.e. they are just reiterating the property setting that they're being notified of, rather than setting a property value in some different object.SetBinding()
calls is thegridPager
object, which is just aGrid
. It doesn't have anyRows
,Columns
, orSource
property to set.Assuming for a moment that we would use binding to accomplish this, you have a third problem:
Pager.Rows
is already the target of the binding established in MainPage.xaml, withPagerTableCategoriesRows
as the source. It cannot also be the target of a binding from any other object (and least of all, a circular binding from itself, which is the only source that would make sense if using thePager.RowsProperty
dependency property, as the code is trying to do).I'm not entirely clear on the wisdom of even doing this. It seems like the
Pager
elements could just bind directly to thePager
properties themselves, thereby inheriting the values from the original view model instead of maintaining a second, completely separate but intended-to-be-identical view model.But assuming, having some very good reason I simply don't understand, you are intent on using two different view models here and want to keep them in sync, it seems to me that you should be able to get it to work by changing your
OnXXXChanged()
handlers so that they set the view model values directly. E.g.:As an aside: I would discourage your use of
as
in the above. The main reason being that if for some reason the cast fails, a less-than-helpfulNullReferenceException
will be your first visible symptom, rather thanInvalidCastException
.I recommend one use
as
only if it is expected that some of the time, the cast will fail. Of course, in such scenarios, you would also always check for anull
result and handle it appropriately.If you intend that the cast will always succeed, then use the cast operator.