是否可以在 MVVM 中拥有用户控件?

Is it possible to have a user control in MVVM?

我想创建一个在许多应用程序中使用的用户控件,我想使用 MVVM 模式。

例如,我有一个带有日历的用户控件,当我点击某一天时,该用户控件会搜索我这一天必须完成的任务。

所以我就在想用户控件里面的逻辑有一个视图模型,就是搜索当天的任务。所以我将用户控件中日历的selectedDate 属性绑定到用户控件的视图模型的属性上,这样当值改变时,视图模型可以搜索当天的任务。

我还希望此用户控件通知主应用程序,即日历中的 selectedDate,因为主应用程序在更改所选日期时必须做另一件事。因此,我尝试将主视图模型中的 属性 绑定到我在用户控件中创建的依赖项 属性,但是用户控件中的 属性 是如何绑定到属性 用户控件的视图模型,更改日期时不会通知主视图模型。

我知道如何在代码隐藏中执行此操作,但我想知道是否可以在 MVVM 中执行此操作,因为用户控件有其自己的逻辑,我想遵循 MVVM 模式。如果不是,当我的应用程序中有许多用户控件时,只有主应用程序使用 MVVM 模式,其余代码在后面,所以我的应用程序中有很大一部分代码在后面,我想避免这种情况。

总而言之,我想知道当我更改日历中的日期时,用户控件如何通知其视图模型并通知应用程序主视图中的绑定 属性。

谢谢。

编辑

最后我得到了我想用事件做的事情,将用户控件的视图模型中的更改传达给更新依赖属性的用户控件的隐藏代码,并且依赖属性通知更改到主视图。代码如下:

XAML主视图:

<Window x:Class="UserControlMvvm.MainView"
        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:UserControlMvvm"
        xmlns:vm="clr-namespace:UserControlMvvm"
        mc:Ignorable="d"
        Name="_mainView"
        Title="MainView" Height="350" Width="525">

    <Window.DataContext>
        <vm:MainViewModel />
    </Window.DataContext>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="102*"/>
            <RowDefinition Height="217*"/>
        </Grid.RowDefinitions>

        <local:ucMyUserControlView HorizontalAlignment="Center" Margin="0,0,0,0" Grid.Row="1" VerticalAlignment="Center"
                                   SelectedDate="{Binding ElementName=_mainView, Path=DataContext.SelectedDateInUserControl, Mode=TwoWay}"/>

        <TextBox x:Name="txtSelectedDateInUserControl" Text="{Binding SelectedDateInUserControl}" HorizontalAlignment="Left" Height="23" Margin="10,35,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120"/>

        <Label x:Name="lblSelectedDate" Content="SelectedDate in UserControl" HorizontalAlignment="Left" Margin="10,0,0,0" VerticalAlignment="Top"/>

        <TextBox x:Name="txtSelectedDateToUserControl" HorizontalAlignment="Right" Height="23" Margin="0,35,5,0" TextWrapping="Wrap" Text="{Binding SelectedDateToUserControl, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="120"/>

        <Label x:Name="lblSeelectedDateToUserControl" Content="Change date on user control" HorizontalAlignment="Right" Margin="0,0,5,0" VerticalAlignment="Top"/>

    </Grid>
</Window>

主视图模型代码:

using System;


namespace UserControlMvvm
{
    class MainViewModel : ViewModelBase
    {
        #region properties
        private DateTime? _selectedDateInUserControl;
        public DateTime? SelectedDateInUserControl
        {
            get { return _selectedDateInUserControl; }
            set
            {
                if(_selectedDateInUserControl != value)
                {
                    SetProperty(ref _selectedDateInUserControl, value);
                    selectedDateInUserControlChanged();
                }
            }
        }

        private string _selectedDateInUserControlText;
        public string SelectedDateInUserControlText
        {
            get { return _selectedDateInUserControlText; }
            set
            {
                if (_selectedDateInUserControlText != value)
                {
                    SetProperty(ref _selectedDateInUserControlText, value);
                }
            }
        }

        private string _selectedDateToUserControl;
        public string SelectedDateToUserControl
        {
            get { return _selectedDateToUserControl; }
            set
            {
                if (_selectedDateToUserControl != value)
                {
                    SetProperty(ref _selectedDateToUserControl, value);
                    DateTime miDateParsed;
                    DateTime.TryParse(value, out miDateParsed);
                    SelectedDateInUserControl = miDateParsed;
                }
            }
        }
        #endregion properties



        #region methods
        /// <summary>
        /// This method is used to do all the tasks needed when the selectedDate in the user control is changed.
        /// </summary>
        private void selectedDateInUserControlChanged()
        {
            try
            {
                //here the code that the main view model has to do when the selected date is changed in the user control.
            }
            catch
            {
                throw;
            }
        }//selectedDateInUserControlChanged
        #endregion methods
    }
}

用户控件XAML:

<UserControl x:Class="UserControlMvvm.ucMyUserControlView"
             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:local="clr-namespace:UserControlMvvm"
             mc:Ignorable="d"
             Name="_ucMyUserControl"
             Width="Auto" Height="Auto">
    <Grid>
        <Calendar HorizontalAlignment="Center" Margin="0,0,0,0" VerticalAlignment="Center"
                  SelectedDate="{Binding ElementName=_ucMyUserControl,Path=DataContext.SelectedDate, Mode=TwoWay}"/>

    </Grid>
</UserControl>

用户控件的代码隐藏,只是为了声明依赖属性并通知视图模型的变化。逻辑在视图模型中。

using System.Windows.Controls;
using System;
using System.Windows;


namespace UserControlMvvm
{
    /// <summary>
    /// Interaction logic for ucMyUserControl.xaml
    /// </summary>
    public partial class ucMyUserControlView : UserControl
    {
        ucMyUserControlViewModel _viewModel;

        public ucMyUserControlView()
        {
            InitializeComponent();

            //The view model is needed to be instantiate here, not in the XAML, because if not, you will get a null reference exception
            //because you try to access to a property when the view model is not still instantiate.
            _viewModel = new ucMyUserControlViewModel();
            DataContext = _viewModel;

            //Events
            _viewModel.SelectedDateChangedEvent += selectedDateChanged;
        }




        #region dependency properties
        //This are the properties that the main view will have available when will use the user control, so dependency properties are the
        //communication way between the main view and the user control.
        //So here you have to declare all the properties that you want to expose to outside, to the main view.

        public static readonly DependencyProperty SelectedDateProperty =
            DependencyProperty.Register("SelectedDate", typeof(DateTime?),
                typeof(ucMyUserControlView), new PropertyMetadata(null, selectedDateChanged));
        public DateTime? SelectedDate
        {
            get
            {
                return (DateTime?)GetValue(SelectedDateProperty);
            }
            set
            {
                //This is the way in which the user control notify to the main view that the selected date is changed.
                SetValue(SelectedDateProperty, value);
            }
        }

        private static void selectedDateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            //This is the way in which the code behind notify to the view model that the main view has changed by the main view.
            ((ucMyUserControlView)d)._viewModel.SelectedDate = e.NewValue as DateTime?;
        }
        #endregion dependency properties



        #region methods to receive notifications from the view model
        //These are the methods that are subcribed to the events of the view model, to know when a property has changed in the view
        //model and be able to notify to the main view.
        private void selectedDateChanged(DateTime? paramSelectedDate)
        {
            try
            {
                //This update the dependency property, so this notify to the main main that the selected date has been changed in the
                //user control.
                SetValue(SelectedDateProperty, paramSelectedDate);
            }
            catch
            {
                throw;
            }
        }//selectedChanged
        #endregion methods to receive notificactions from the view model
    }
}

用户控件的视图模型:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace UserControlMvvm
{
    class ucMyUserControlViewModel : ViewModelBase
    {
        #region events
        //The events are user to notify changes from the properties in this view model to the code behind of the user control, so
        //later the user control can notify the changes from the code behind to the main view.
        //This is because the user control only can notify changes to the main view from the dependency properties and the dependency properties
        //are declared in the code behind. So to communicate changes from the view model to the code behind it is needed the use of an event.

        //So the changes are notify in this way:
        //view model --> code behind --> main view

        public delegate void SelectedDateChangedEventHandler(DateTime? paramSelectedDate);
        public event SelectedDateChangedEventHandler SelectedDateChangedEvent;


        private void OnSelectedDateChanged(DateTime? paramTipoSeleccionado)
        {
            try
            {
                //Here notify to the code behind of the user control that the selectedDate is changed.
                SelectedDateChangedEvent?.Invoke(paramTipoSeleccionado);
            }
            catch
            {
                throw;
            }
        }//OnSelectedDateChanged
        #endregion events



        #region properties
        private DateTime? _selectedDate;
        public DateTime? SelectedDate
        {
            get { return _selectedDate; }
            set
            {
                if(_selectedDate != value)
                {
                    SetProperty(ref _selectedDate, value);
                    selectedDateChanged();
                    OnSelectedDateChanged(SelectedDate);
                }
            }
        }
        #endregion properties



        #region methods
        private void selectedDateChanged()
        {
            try
            {
                //Here the code that the user control has to execute when the selectedDate is changed.
            }//try
            catch
            {
                throw;
            }
        }
        #endregion methods
    }
}

最后,实现 INotifyPropertyChanged 的​​ class,它可以是您喜欢的任何实现,但也许对某些人来说可能很有趣:

/*
 * Class that implements the INotifyPropertyChanged that it is used by all view models to
 * notifiy changes in their properties to the view.
 */

using System.Runtime.CompilerServices;
using System.ComponentModel;

namespace UserControlMvvm
{
    public class ViewModelBase : INotifyPropertyChanged
    {
        protected virtual void SetProperty<T>(ref T member, T val,
            [CallerMemberName] string propertyName = null)
        {
            if (object.Equals(member, val)) return;

            member = val;
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        public event PropertyChangedEventHandler PropertyChanged = delegate { };
    }
}

使用此解决方案,我们可以看到,如果我更改日历中的日期,主视图中的第一个文本框会更新,但不会更新第二个文本框,因为它未绑定到用户控件。

如果我在主视图中更改第一个文本框中的日期,用户控件日历中选定的日期也会更新,但不会更新第二个文本框。

如果我更改第二个文本框中的日期,它会在日历中更改,因为我更新了视图模型的 selectedItemInUserControl 属性,并且此 属性 通知用户控件日历的变化。

所以通过这个解决方案,我可以在用户控件中使用 MVVM 模式,它只公开依赖属性以与主视图通信。

是的。如果您使用的框架使用导航系统在 View/ViewModels 之间移动,您可以调整它以启动您的 UserControl View/ViewModel。视图是 Window 还是 UserControl.

并不重要

编辑

也可以使用 Messenger 系统(同样在大多数 MVVM 框架中可用)在视图模型之间传递信息,因此当控件的 ViewModel 中的 属性 发生变化时,它可以发送向主 ViewModel 发送消息以更改其 属性(s)