65.9K
CodeProject 正在变化。 阅读更多。
Home

Silverlight MVVM概念的完全简单介绍

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (21投票s)

2011年5月3日

CPOL

7分钟阅读

viewsIcon

91477

downloadIcon

3762

在本文中,我将 Silverlight 中的 MVVM 模式的使用减少到最少的代码行,以便理解基础知识。

注意:您可能会收到一条错误消息,提示 Windows 已禁用从 Internet 下载的 DLL。如果发生这种情况,请通过资源管理器导航到这些文件,然后在“属性”显示中,您会看到一个名为“取消阻止”的按钮。单击该按钮,您将能够编译。

为什么选择 MVVM?

根据我编程的经验,我一直以“复杂性是敌人”作为指导原则。作为程序员,我们在构建程序/系统到一定程度时做得相当好。超过这一点,生产力就会开始显著下降。我们都有过这样的经历。您知道代码在哪里,但项目已达到一定规模,查找内容开始耗费时间。然后,您就无法再以“思维的速度”工作,而是以检索的速度工作。

模式是解决系统中“复杂性”问题的一种有趣方法。我一直认为模式是一种隔离或疫苗,可以应对系统复杂性。在某种程度上,您在编码过程的开始就增加了复杂性,希望以后能限制复杂性。所谓的 MVVM 模式,我认为就是一个很好的例子。不使用“代码隐藏”的想法是违反直觉的。没有什么比做一个简单的基于事件的应用程序更快了,其中 UI 在 XAML 中,编码在代码隐藏中。但是,随着系统的增长,您很快就会达到生产力平台期。我们都有过这样的经历。我知道我有。

通过 MVVM,我们只需最大限度地利用 Silverlight 中的“绑定”概念。我们都熟悉数据绑定,但通过“命令”绑定的潜力,我们可以将 UI(视图)与处理(ViewModel)完全分离。此模式中的抽象流程是 Model<->ViewModel<->View。这带来了“关注点分离”这一备受吹捧的副产品。这种概念是,在开发中,将 UI 的开发与处理(连接)完全分开是一个巨大的优势,这样 UI 部分的过程就可以被替换而不会在代码中导致异常。从可测试性的角度来看,MVVM 也被认为是一个很好的策略。

从编码人员的角度来看,问题在于这是一个依赖于不明显语法和约定的新范例。无论是样式还是诸如将数据加载到页面、响应事件和验证等基本功能,都存在学习曲线。

当面对大量的“新”技术时,我的策略是尝试将这些技术分解为最简单的实现。我花了很长时间寻找一个好的例子,虽然有很多关于 MVVM 的好文章,但我希望一次性涵盖 MVVM 的所有基础知识。

  • 关注点分离
  • 数据绑定
  • 命令绑定
  • 验证
  • 单元测试
  • 链接到框架(Prism)

应用程序的 UI 非常简单。

MainPage.PNG

您可以从文本框添加数据,它会显示在数据网格中。如果您尝试添加过长的数据,您将收到字段的错误消息和弹出窗口。虽然此验证看起来有点过头,但我只是试图展示 MVVM 策略的各种“活动部件”。

XAML

<UserControl x:Class="MVVMtest1.MainPage"
    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:app="clr-namespace:MVVMtest1"
    mc:Ignorable="d"
    d:DesignHeight="324" d:DesignWidth="459" 
	xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" >
    <UserControl.DataContext>
        <app:HelloWorldModel />
    </UserControl.DataContext>
    <Grid x:Name="LayoutRoot" Background="White" Height="326" Width="469">
        <sdk:DataGrid AutoGenerateColumns="true" Height="182" 
	ItemsSource="{Binding Path=myData}" 
                      HorizontalAlignment="Left" Margin="159,107,0,0" 
                      Name="dataGrid1" VerticalAlignment="Top" Width="108" >            
        </sdk:DataGrid>
        <Button Content="Add Data" Height="23" HorizontalAlignment="Left" 
                Command="{Binding SaveCommand}"  
                CommandParameter="{Binding Text,ElementName=txtText,Mode=TwoWay, 
                ValidatesOnDataErrors=True}"
                Margin="96,45,0,0" Name="btnAdd" VerticalAlignment="Top" Width="75" />
        <TextBox TabIndex="0" Height="23" Text="{Binding inputValue, Mode=TwoWay, 
                    ValidatesOnDataErrors=True}" HorizontalAlignment="Right" 
			Margin="0,44,163,0" 
                    Name="txtText" VerticalAlignment="Top" Width="120"  />
        <TextBlock Height="23" HorizontalAlignment="Left" Margin="150,12,0,0" 
		Name="textBlock1" 
                   Text="MVVM Hello World Example" VerticalAlignment="Top" 
			FontWeight="Bold" />
        <TextBlock Height="23" HorizontalAlignment="Left" 
		Margin="96,78,0,0" Name="textBlock2"
                   Text="Please limit your data to 10 characters" 
			VerticalAlignment="Top" Width="225" />
        <Grid Visibility="{Binding Path=MessageVisibility}">
            <Grid.RowDefinitions>
                <RowDefinition Height="2*" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Rectangle Grid.RowSpan="2" Fill="Black" Opacity="0.08" />
            <Border Grid.Row="0" BorderBrush="blue" BorderThickness="1" CornerRadius="10"
                        Background="White"
                        HorizontalAlignment="Center" VerticalAlignment="Center">
                <Grid Margin="10">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="auto" />
                        <RowDefinition Height="40" />
                    </Grid.RowDefinitions>
                    <TextBlock Text="{Binding Path=Message}" 
                               MinWidth="150"
                               MaxWidth="300"
                               MinHeight="30"
                               TextWrapping="Wrap" Grid.Row="0" Margin="10, 5, 10, 5" />
                    <Button Content="OK" Grid.Row="1" 
                            Margin="5" Width="100"
                            Command="{Binding Path=HideMessageCommand}"/>
                </Grid>
            </Border>
        </Grid>
    </Grid>
</UserControl>

虽然 MVVM 中有很多“魔法”之处,但第一点是数据绑定。下面的代码片段使用以下代码将整个用户控件的数据绑定到 ViewModel

<UserControl.DataContext>
    <app:HelloWorldModel />
</UserControl.DataContext>

通过这种数据绑定,用户控件中的所有控件都可以绑定到 ViewModel 的属性。同样,“命令”和“事件”可以绑定到 ViewModel 中的“命令属性”。首先,我们来看看简单的绑定。DataGrid ItemSource 绑定到“myData”属性。由于此属性定义为 ObservableCollection,因此此数据绑定隐式为双向,并且此集合中的任何更改都会“自动”显示在屏幕上。

ItemsSource="{Binding Path=myData}"

接下来,我们将绑定到按钮的命令事件。

Command="{Binding SaveCommand}"  
                CommandParameter="{Binding Text,ElementName=txtText,Mode=TwoWay, 
                ValidatesOnDataErrors=True}"

此绑定的要点是:

  • 我们绑定到 Command 属性,而不是 click 事件。

    这是 Silverlight 4.0 内置的基本命令功能。绑定到事件超出了本文的范围。

  • 我们可以为该命令定义一个参数,该参数是另一个控件的属性。
  • 在此参数中,我们可以通过添加 ValidatesOnDataErrors=True 语法来添加验证。

ViewModel

现在 XAML 已设置好,让我们看看 ViewModel 代码,了解一切是如何工作的。

using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System.Windows;
using Microsoft.Practices.Prism.Commands;

namespace MVVMtest1
{
    public class HelloWorldModel : INotifyPropertyChanged, IDataErrorInfo
    {
        private string _inputValue;
        private string _message;
        private bool isError = false;

        public HelloWorldModel()
        {
            LoadData();
            DefineCommands();
            MessageVisibility = Visibility.Collapsed;
        }
        
        public string Message
        {
            get { return _message; }
            set
            {
                _message = value;
                NotifyPropertyChanged("Message");
            }
        }
                                                                                         
        public string inputValue
        {
            get { return _inputValue; }
            set
            {
                _inputValue = value;
                OnPropertyChanged("inputValue");
            }
        }

        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private ObservableCollection<dataitemclass> _myData;

        public ObservableCollection<dataitemclass> myData
        {
            get
            {
                return _myData;
            }
        }

        private void LoadData()
        {
            _myData = new ObservableCollection<dataitemclass>();           
            _myData.CollectionChanged += 
		new System.Collections.Specialized.NotifyCollectionChangedEventHandler
		(_myData_CollectionChanged);
            _myData.Add(new DataItemClass { Id= _myDataId, Name= "first line" });
            _myData.Add(new DataItemClass { Id = _myDataId, Name = "second line" });
            _myData.Add(new DataItemClass { Id = _myDataId, Name = "third line" });
        }

        int _myDataId = 1;
        
        void _myData_CollectionChanged(object sender, 
		System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            _myDataId++;
        }        

        private Visibility messageVisibility;
        public Visibility MessageVisibility
        {
            get { return messageVisibility; }
            set
            {
                if (messageVisibility != value)
                {
                    messageVisibility = value;
                    NotifyPropertyChanged("MessageVisibility");
                }
            }
        }

        protected void NotifyPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }        

        private ICommand _SaveCommand;
        public ICommand SaveCommand
        {
            get
            {
                return _SaveCommand;
            }
        }

        private ICommand _HideMessageCommand;
        public ICommand HideMessageCommand
        {
            get
            {
                return _HideMessageCommand;
            }
        }

        private void DefineCommands()
        {
            // this uses the prism framework but could also use a simple class 
            //(ICommandImplementation.cs)
            _SaveCommand = new DelegateCommand<string> (OnSaveCommand);
            _HideMessageCommand = new DelegateCommand(OnHideMessageCommand);
        }

        private void OnHideMessageCommand()
        {
            MessageVisibility = Visibility.Collapsed;
        }

        private void OnSaveCommand(string s)
        {
            if (!isError)
            {
                _myData.Add(new DataItemClass { Id = _myDataId, Name = s });
                this.inputValue = "";
            }
        }

        public string Error
        {
            get { return null; }
        }

        public string this[string propertyName]
        {
            get
            {
                if (propertyName == "inputValue")
                {
                    if (this.inputValue != null)
                    {
                        if (this.inputValue.Length > 10)
                        {
                            isError = true;
                            MessageVisibility = Visibility.Visible;
                            _message = "Why are you typing such long words";
                            Message = _message;
                            return _message;
                        }
                        else
                        {
                            isError = false;
                        }
                    }
                }
                return null;
            }
        }
    }

    public class DataItemClass 
    {       
        public int Id { get; set; }      
        public string Name { get; set; }           
    }
}

此页面上有很多魔法。请注意,此类继承自 INotifyPropertyChanged IDataErrorInfo。这些接口提供了我们“免费”获得的此类与 XAML 之间的交互。因此,当我们将项添加到 myData 属性背后的集合中时,UI 上的 datagrid 就会“自动”更新。同样,当发生属性“get”时,事件会触发“public string this[string propertyName]”属性上的“get”,而这个 get IDataErrorInfo 实现的基础。所有这些功能都由对构造函数的调用启用,该构造函数是 XAML 中数据绑定结果的结果。

此类处理 XAML 命令的能力是实现 MVVM 的关键功能。命令绑定到 ViewModel 上的属性,这些属性会调用事件。为了使此策略奏效,必须有一些实现 ICommand 的代码。您可以使用各种“框架”,从简单的单个类(我在代码示例中包含了一个)到 Microsoft Patterns and Practices 团队发布的开源 Prism。我使用了 Prism(对于这个项目来说有点过度),这涉及到引用 DLL 并添加一个 using

using Microsoft.Practices.Prism.Commands;

命令结构所需的三个代码“部分”如下所示:

// the property referenced in the XAML
private ICommand _SaveCommand;
public ICommand SaveCommand
{
    get
    {
        return _SaveCommand;
    }
}

// called by the constructor
private void DefineCommands()
{
    // this uses the prism framework but could also use a simple class
    // (ICommandImplementation.cs)
    _SaveCommand = new DelegateCommand<string> (OnSaveCommand);
    _HideMessageCommand = new DelegateCommand(OnHideMessageCommand);
}

// this is the actual 'work'
private void OnSaveCommand(string s)
{
    if (!isError)
    {
        _myData.Add(new DataItemClass { Id = _myDataId, Name = s });
        this.inputValue = "";
    }
}

然后,项目可以实现 ICommand。这是基本绑定和命令结构完全简单的实现。

过去所有试图“管理”基本显示和 CRUD 任务的尝试都存在 5% 原则问题。 “框架”在 95% 的所需功能方面运作良好,但您将花费 95% 的项目开发时间来完成最后 5% 的功能。希望 MVVM 不会像许多其他框架一样被淘汰。MVVM 模式的实现还有一些更关键的部分,它们体现了该技术对于企业开发的适用性,例如验证和可测试性。

验证

我在代码中包含了一个简单的验证示例。我们实现 IDataErrorInfo,其基本实现代码如下:

public class ErrorClass : IDataErrorInfo    
{
    public string Error
    {
        get { throw new NotImplementedException(); }
    }

    public string this[string columnName]
    {
        get { throw new NotImplementedException(); }
    }
}

您可以在我上面的类代码中查看如何将其连接到您的 ViewModel 类。在调试时设置断点以跟踪此功能的处理流程很有帮助,因为它相当复杂。

error.PNG

消息框

在没有代码隐藏的情况下,通过模态消息框向用户发送消息会更加困难。我已包含相关代码。在此示例中,如果用户尝试输入超过 10 个字符的数据,则 TextBox 会显示错误,并显示模态消息框。

popup.PNG

测试 MVVM

最后,我包含了一些测试。在使用 MVVM 测试 Silverlight 时首先要记住的是,请勿使用标准测试框架,该框架可以随典型的 Visual Studio 项目一起安装。请使用 Silverlight Unit Test Application 模板。示例中的第一个测试显示了如何测试“命令”,第二个测试显示了如何测试验证。

runTests.PNG

测试模板有一个有用的屏幕显示,允许运行所有测试或选定的测试组。

结果如下所示:

units.PNG

我试图将 MVVM 模式的基本实现的示例做到最简单。当然,构建 Silverlight 应用程序还有很多其他方面,例如转换器和行为,但希望这能帮助您克服可怕的初始学习曲线。

© . All rights reserved.