How can I bind custom properties from View to View

2019-07-16 21:16发布

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();
            }
        }

        ...

    }
}

4条回答
趁早两清
2楼-- · 2019-07-16 22:00

This solution assumes :

  1. Parent window/page knows nothing about UC. But UC knows about its parent.
  2. UC is relying on the DataContext which it gets from its parent propagated down to it.

What this code does :

  1. UC creates a 2-way binding with MainViewModel's MainWinProp1 property.
  2. Any changes made in UC are visible in MainViewModel and vice-versa.

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.

查看更多
再贱就再见
3楼-- · 2019-07-16 22:05

Your main confusion comes due to the fact that Pager is not a view, but a UserControl.

They both can inherit from UserControl class, but the difference is: In MVVM a View (or a Subview aka DataTemplate) has a ViewModel bound to it (via DataContext) 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 a UserControl 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). The UserControl will expose the DPs for external data-binding. In a View 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 a UserControl it doesn't require the PagerViewModel and it's code should be moved in the Pager'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!!!).

using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
// MVVM Violation, it's part of WPF 
using System.Windows.Data;
// MVVM Violation, it's part of WPF (usually PresentationFramework.dll)
using System.Windows.Media;
using Client.Model;

namespace Client.ViewModel
{
    public class PagerViewModel : ViewModelBase
    {

        ...

        // MVVM Violation, it's a type from Assembly:  PresentationFramework (in PresentationFramework.dll)
        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();
            }
        }

        ...

    }
}

Enforcing MVVM is much easier if you create separate assemblies.

  1. MyApp.Desktop/MyApp.UniversalApp/MyApp.Web: This are allowed to have references to PresentationFramework.dll and all other assemblies below
  2. MyApp.ViewModels: This assembly is only allowed to contain ViewModels. References only to Model, Interfaces, Domain. Referebces to Presentation.dll and MyApp.Data.*|MSSQL|Oracle|MySQL|SAP are strictly forbidden.
  3. MyApp.Core/MyApp.Shared/MyApp.Domain: Contains your business Logic. References Models and Infrastructure
  4. (Optional)MyApp.Models: Your models and nothing else.
  5. (Optional)MyApp.Infrastructure/MyApp.Abstractions: Contains your service interfaces (for repositories or services). Reference nothing or just Models
  6. (Optional)MyApp.Data.*|MSSQL|Oracle|MySQL|SAP: Your DB Specific implementations and everything that's not part of your domain. Reference only to Infrastructure and domain

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.)

查看更多
萌系小妹纸
4楼-- · 2019-07-16 22:20

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: enter image description here

regards,

查看更多
The star\"
5楼-- · 2019-07-16 22:22

There are two obvious problems with the code you posted:

  1. Your 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.
  2. You are attempting to set bindings on properties that don't exist. I.e. the target of your code-behind SetBinding() calls is the gridPager object, which is just a Grid. It doesn't have any Rows, Columns, or Source property to set.

Assuming for a moment that we would use binding to accomplish this, you have a third problem:

  1. You would be trying to bind two different source properties to the same target property. E.g. Pager.Rows is already the target of the binding established in MainPage.xaml, with PagerTableCategoriesRows 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 the Pager.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 the Pager 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.:

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));

    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();
    }

    private void ListBoxEntriesOnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        SelectedEntry = (sender as ListBox).SelectedItem;
    }

    private static void OnSourceChanged(DependencyObject pager, DependencyPropertyChangedEventArgs e)
    {
        ((PagerViewModel)(pager as Pager).gridPager.DataContext).Collection =
            (ObservableCollection<IPagableEntry>)e.NewValue;
    }

    private static void OnRowsChanged(DependencyObject pager, DependencyPropertyChangedEventArgs e)
    {
        ((PagerViewModel)(pager as Pager).gridPager.DataContext).Rows =
            (int)e.NewValue;
    }

    private static void OnColumnsChanged(DependencyObject pager, DependencyPropertyChangedEventArgs e)
    {
        ((PagerViewModel)(pager as Pager).gridPager.DataContext).Columns =
            (int)e.NewValue;
    }
}

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-helpful NullReferenceException will be your first visible symptom, rather than InvalidCastException.

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 a null result and handle it appropriately.

If you intend that the cast will always succeed, then use the cast operator.

查看更多
登录 后发表回答