MVVM正确的方式

时间:2012-11-26 09:47:32

标签: c# wpf mvvm binding

我在其他关于如何实现MVVM的问题中得到了提示。在Student类本身进行更改时(我在项目中发生了很多变化),我在将绑定更新传递给GUI时遇到了问题。有没有办法简化这些事情并以比实现更紧凑的方式使用它?或者这是实现MVVM的最先进技术吗?

class MainWindowViewModel : INotifyPropertyChanged
{
   ObservableCollection<StudentViewModel> studentViewModels = new ObservableCollection<StudentViewModel>();

   public ObservableCollection<StudentViewModel> StudentViewModels
   {
      get { return studentViewModels; }
   }

   public MainWindowViewModel()
   {
      studentViewModels.Add(new StudentViewModel());
      studentViewModels.Add(new StudentViewModel());
      studentViewModels.Add(new StudentViewModel());
   }

   public event PropertyChangedEventHandler PropertyChanged;
   internal void OnPropertyChanged(String propertyName)
   {
      if (PropertyChanged != null)
      {
         PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
      }
   }
}


class StudentViewModel : INotifyPropertyChanged
{
   Student model;
   public String StudentFirstName
   {
      get { return model.StudentFirstName; }
      set { model.StudentFirstName = value; }
   }
   public String StudentLastName
   {
      get { return model.StudentLastName; }
      set { model.StudentLastName = value; }
   }

   public StudentViewModel()
   {
      model = new Student();
      this.model.PropertyChanged += (sender, e) => 
      {
         switch (e.PropertyName)
         {
            case "StudentFirstName": OnPropertyChanged("StudentFirstName"); break;
            case "StudentLastName": OnPropertyChanged("StudentLastName"); break;
            default: break;
         }
      };
   }

   public StudentViewModel(Student model)
   {
      this.model = model;

      this.model.PropertyChanged += (sender, e) =>
      {
         switch (e.PropertyName)
         {
            case "StudentFirstName": OnPropertyChanged("StudentFirstName"); break;
            case "StudentLastName": OnPropertyChanged("StudentLastName"); break;
            default: break;
         }
      ;
   }

   public void changeStudent()
   {
      model.changeStudent();
   }

   public event PropertyChangedEventHandler PropertyChanged;
   internal void OnPropertyChanged(String propertyName)
   {
      if (PropertyChanged != null)
      {
         PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
      }
   }
}



class Student : INotifyPropertyChanged
{
   public String studentFirstName;
   public String StudentFirstName
   {
      get { return studentFirstName; }
      set 
      {
         if (studentFirstName != value)
         {
            studentFirstName = value;
            OnPropertyChanged("StudentFirstName");
         }
      }
   }
   public String studentLastName;
   public String StudentLastName
   {
      get { return this.studentLastName; }
      set
      {
         if (studentLastName != value)
         {
            studentLastName = value;
            OnPropertyChanged("StudentLastName");
         }
      }
   }

   public Student() { }

   public void changeStudent()
   {
      StudentLastName = "McRonald";
   }

   public event PropertyChangedEventHandler PropertyChanged;
   internal void OnPropertyChanged(String propertyName)
   {
      if (PropertyChanged != null)
      {
         PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
      }
   }
}

4 个答案:

答案 0 :(得分:7)

首先,我建议使用其中一个MVVM框架(我个人喜欢并使用Caliburn.Micro,但还有MVVM Lightmyriad of others)。

(我将从现在开始使用Caliburn.Micro实例作为示例,因为这是我或多或少知道的一个框架)

为什么呢?好吧,它为您提供强类型NotifyOfPropertyChange(),内置事件聚合器,窗口管理器等等。这样你就不必每次都重新发明轮子。此外,Caliburn.Micro的引导程序允许您烘焙您选择的IoC容器,如果您想要使用MVVM而不使用框架方式,那么使用WPF并不是那么容易。 作为奖励,您可以拦截GUI控件中的事件,因此您实际上不必在代码隐藏中编写任何内容。

某些框架允许您按惯例进行绑定并简化命令,但是您必须阅读它,具体取决于您使用的框架。

其次,我非常支持完全重写视图模型,因此它们是独立的类而不是数据模型的包装器。您可以稍后使用AutomapperValueInjecter进行映射。

那么你就有了视图模型,例如:

public class StudentViewModel : PropertyChangedBase
{
    private string firstName;
    public string FirstName
    {
        get { return firstName; }
        set
        {
            firstName = value;
            NotifyOfPropertyChange(() => FirstName);
        }       
    }

    private string lastName
    public string LastName
    {
        get { return lastName; }
        set
        {
            lastName = value;
            NotifyOfPropertyChange(() => LastName);
        }       
    }
}

这就是视图模型。然后在视图模型中使用数据绑定,验证等。

您的Student课程可以是一个简单的DTO或EF级或其他什么。为简单起见,让我们选择哑巴DTO:

public class Student
{
    public string FirstName { get;set; }
    public string LastName { get;set; }
}

因此,只有在保存到数据库时才使用DTO。就是这样。对于“常规”应用程序使用,GUI交互(绑定)使用视图模型。

这就是Automapper / ValueInjecter发挥作用的地方,因为当您想要“保存”更改/在任何地方添加新学生时,您必须将视图模型映射到模型,例如:

//ValueInjecter
var dataModel = new Student().InjectFrom(this) as Student;
//AutoMapper
var dataModel = Mapper.Map<StudentViewModel, Student>(this);

就是这样。简单,容易,干净。您描述它的方式,您想要更改基础模型。我建议不要这样做,操作视图模型而不是可以通知你的UI。您仅使用模型“修改”数据存储中的数据(保存/更新/获取/删除)或以某种方式“传输”数据(例如,使用REST Web服务),并使用视图模型进行交互。

答案 1 :(得分:1)

如果您在StudentViewModel课程中创建学生课程怎么办?此外,ViewModelBase类可以简化代码(或者至少缩短代码)。

class MainWindowViewModel : ViewModelBase
{
    public ObservableCollection<StudentViewModel> StudentViewModels { get; private set; }
    public MainWindowViewModel()
    {
        StudentViewModels = new ObservableCollection<StudentViewModel>();
    }
}

class StudentViewModel : ViewModelBase
{
    public Student Student { get; private set; }

    public StudentViewModel()
    {
        Student = new Student();
    }

    public StudentViewModel(Student model)
    {
        Student = model;
    }

    public void ChangeStudent()
    {
        Student.changeStudent();
    }
}

public class Student : ViewModelBase
{
    public String studentFirstName;
    public String StudentFirstName
    {
        get { return studentFirstName; }
        set
        {
            if (studentFirstName != value)
            {
                studentFirstName = value;
                OnPropertyChanged("StudentFirstName");
            }
        }
    }
    public String studentLastName;
    public String StudentLastName
    {
        get { return this.studentLastName; }
        set
        {
            if (studentLastName != value)
            {
                studentLastName = value;
                OnPropertyChanged("StudentLastName");
            }
        }
    }

    public Student() { }

    public void changeStudent()
    {
        StudentLastName = "McRonald";
    }
}

这是一个实现ViewModelBase接口的INotifyPropertyChanged类:

public abstract class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged(string propName)
    {
        var eh = PropertyChanged;
        if (eh != null)
        {
            eh(this, new PropertyChangedEventArgs(propName));
        }
    }
}

进行测试:

<Grid>
    <ListBox Name="lbStudents" ItemsSource="{Binding StudentViewModels}" >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Path=Student.StudentLastName}" />
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

和MainWidnow的构造函数

 public MainWindow()
    {
        InitializeComponent();

        var viewModel = new MainWindowViewModel();
        var student = new Student { StudentFirstName = "John", StudentLastName = "Doe" };
        viewModel.StudentViewModels.Add(new StudentViewModel(student));

        DataContext = viewModel;
        MouseLeftButtonDown += new MouseButtonEventHandler((object sender, MouseButtonEventArgs e) =>
        {
            viewModel.StudentViewModels[0].ChangeStudent();
        });
    }

如果您点击该窗口,系统会调用第一个ChangeStudent的{​​{1}}方法,同时也会更新用户界面。

答案 2 :(得分:1)

我同意其他答案,你应该看看MVVM框架。我使用MVVM Foundation来完成我的工作。

文字墙将随之而来。我不久前也开始使用MVVM。在我的上一个项目中,这段代码对我很有帮助。

最近我不得不处理一个类,它还需要IEditableObject来管理编辑/保存/丢弃对象以及PropertyChanged通知。既然你提到了“轻松的事情”的方法,我将发布我曾经将MVVM和可编辑对象结合在一起的基础classe。对我来说,实施所有其他课程是一个很大的时间。

EditableObject继承自MVVM Foundation的ObservableObject。它将您正在使用的对象的结构作为类型参数。

如果您熟悉IEditabeObject的实现,则有editData和backupData变量来保存您当前正在使用的数据(我不是从这里继承的,我创建了自己的EditableObject)。我正在使用AutoMapper来创建我正在使用的数据的深层副本(备份),以便可以恢复它。还有其他方法可以执行此操作(查找序列化或值注入),但我已经在项目中有AutoMapper,因此不需要更多dll。

EditableObject具有抽象方法SaveObject和RemoveObject,您可以使用它们来处理数据库调用等,以删除和保存对象。使用BeginEdit和DiscardChanges以及SaveChanges完成对象的编辑。

魔法发生在RaisePropertiesChanged方法中,该方法在类中的所有修饰属性上引发PropertyChanged方法。因此,每当您编辑对象时(假设)都会丢弃更改。 UI将刷新并返回原始值。它还包括可以将UI绑定到的IsEditEnabled标志。最初我使用带有空字符串的PropertyChanged,但这会在所有属性上引发它。使用属性我确保它仅在我需要更新的属性上更改。

我使用你的学生班来实现这个并附上下面的基类。

希望它有所帮助!

public class Student: EditableObject<WPF.MVVMBase.Student.StudentData>
    {
        #region Struct

        public struct StudentData
        {
            public string firstName;
            public string lastName;
        }

        #endregion

        #region Public Properties

        [ObservableProperty]
        public string FirstName
        {
            get
            {
                return _editData.firstName;
            }
            set
            {
                _editData.firstName = value;
                this.RaisePropertyChanged("FirstName");
            }
        }

        [ObservableProperty]
        public string LastName
        {
            get
            {
                return _editData.lastName;
            }
            set
            {
                _editData.lastName = value;
                this.RaisePropertyChanged("LastName");
            }
        }

        #endregion

        #region Methods

        protected override bool SaveObject()
        {
            //Save Student Changes to Database

            return true;
        }

        protected override bool RemoveObject()
        {
            //Remove Student from Database

            return true;
        }

        #endregion
    }

这是EditableObject类

namespace WPF.MVVMBase
{
    /// <summary>
    /// Property Decorator that marks the Property as Observable. This is used by the EditableObject class to determine for which properties to raise the Property Changed method
    /// </summary>
    public class ObservablePropertyAttribute : System.Attribute{};

    /// <summary>
    /// Extends the ObservableObject class. EditableObject implements methods which are used to edit the object as well as raise the Property Changed events.
    /// </summary>
    /// <typeparam name="T">The Struct for the Editable Object</typeparam>
    public abstract class EditableObject<T> : ObservableObject
    {
        #region Private Variables

        bool _IsEditEnabled = false;
        bool _IsSelected = false;

        protected T _editData;
        protected T _backupData;

        #endregion

        #region Public Properties

        /// <summary>
        /// Controls if the Edit is enabled on the Editable Object
        /// </summary>
        public bool IsEditEnabled
        {
            get
            {
                return _IsEditEnabled;
            }
            protected set
            {
                _IsEditEnabled = value;
                this.RaisePropertyChanged("IsEditEnabled");
            }
        }

        /// <summary>
        /// Determines weather the object is Selected. Used with Lists
        /// </summary>
        public bool IsSelected
        {
            get
            {
                return _IsSelected;
            }
            set
            {
                _IsSelected = value;
                this.RaisePropertyChanged("IsSelected");
            }
        }

        #endregion

        #region Constructor

        public EditableObject()
        {
            //Create an instance of the object that will hold the data.
            _editData = Activator.CreateInstance<T>();
        }

        #endregion

        #region Methods

        #region Abstract Methods

        /// <summary>
        /// Handle the object saving. This is called by the SaveChanges method.
        /// </summary>
        /// <returns>Indicates if the object was saved successfully</returns>
        protected abstract bool SaveObject();

        /// <summary>
        /// Handle the object remove. This is called by the Remove method.
        /// </summary>
        /// <returns>Indicates if the object was removed successfully</returns>
        protected abstract bool RemoveObject();

        #endregion

        /// <summary>
        /// Begin editing the object. Sets the IsEditEnabled to true and creates a backup of the Data for restoring.
        /// </summary>
        public void BeginEdit()
        {
            IsEditEnabled = true;
            _backupData = Mapper.DynamicMap<T>(_editData);
        }

        /// <summary>
        /// Discard any changes made to the object. Set the IsEditEnabled flag to false and restore the data from the Backup.
        /// </summary>
        public void DiscardChanges()
        {
            _editData = _backupData;
            IsEditEnabled = false;

            RaisePropertiesChanged(this);
        }

        /// <summary>
        /// Save the changes made to the object. Calls the SaveObject method. If save was successfull IsEditEnabled is set to false and backup data is set to current data.
        /// </summary>
        /// <returns>Indicates if the object was saved successfully</returns>
        public bool SaveChanges()
        {
            bool isSaveSuccessfull = SaveObject();

            if (isSaveSuccessfull == true)
            {
                _backupData = _editData;

                IsEditEnabled = false;

                RaisePropertiesChanged(this);
            }

            return isSaveSuccessfull;
        }

        public bool Remove()
        {
            bool isRemoveSuccessfull = RemoveObject();
            return isRemoveSuccessfull;
        }

        /// <summary>
        /// Raises ObservableObject Property Changed for all the decorated methods in the given object so that the interface can refresh accordingly.
        /// </summary>
        /// <param name="baseObject"></param>
        public void RaisePropertiesChanged(object baseObject)
        {
            PropertyInfo[] properties = baseObject.GetType().GetProperties();
            foreach (PropertyInfo property in properties)
            {
                object[] attributes = property.GetCustomAttributes(true);

                bool isObservableProperty = (from attribute in attributes
                                             where attribute is ObservablePropertyAttribute
                                             select attribute).Count() > 0;

                if (isObservableProperty)
                {
                    RaisePropertyChanged(property.Name);
                }
            }
        }

        #endregion
    }
}

答案 3 :(得分:0)

我将我的INotifyPropertyChanged代码放入基类:

public abstract class PropertyChangedBase: INotifyPropertyChanged
{
    protected PropertyChangedBase()
    {
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string propertyName)
    {
        var propertyChangedEventArgs = new PropertyChangedEventArgs(propertyName);
        PropertyChangedEventHandler changed = PropertyChanged;
        if (changed != null)
        {
            changed(this, propertyChangedEventArgs);
        }
    }
}

然后在需要属性更改事件的每个类中,将其添加为基类:

class Student : PropertyChangedBase
{
    public String StudentFirstName
    {
        get { return model.StudentFirstName; }
        set 
          { 
            model.StudentFirstName = value;
            this.OnPropertyChanged("StudentFirstName"); 
          }
    }

    public String StudentLastName
    {
       get { return model.StudentLastName; }
       set 
          { 
             model.StudentLastName = value;
             this.OnPropertyChanged("StudentLastName");
          }
    } 
}

我对你的代码感到困惑的一件事是,为什么你有一个studentview模型?在MVVM中,你有你的模型,你的对象设计 - 在这种情况下'学生'然后你有一个View,它将是MainWindow.xaml,然后你的ViewModel是MainWindowViewModel。所以你真的不需要studentViewModel。 所以你的MainViewModel应该是这样的:

class MainWindowViewModel : PropertyChangedBase
{
   ObservableCollection<Student> _Students = new ObservableCollection<Student>();

   public ObservableCollection<Student> Students
   {
      get { return _Students; }
   }

public MainWindowViewModel()
{
  _Students.Add(new Student() { StudentFirstName = "Foo", StudentLastName = "Bar" });
  _Students.Add(new Student() { StudentFirstName = "John", StudentLastName = "Doe" });
  _Students.Add(new Student() { StudentFirstName = "Emy", StudentLastName = "Bob" });
}