这几天看了一些关于Silverlight的设计模式相关的文章,主流观点是:Silverlight + WCF + MVVM + Prism,而这几个都不太了解,汗啊~
没关系,“没吃过猪肉还没见过猪跑吗”,既然我们自己的项目中没有用到,那就找一些别人写好的吧,边看边学,边学边用,人总是要不断前进的,俗话说,学习如逆水行舟,不进则退,得继续努力了!
还是使用之前《Silverlight之路》中使用的项目,先以登录页面为入口,尝试使用MVVM进行开发,先建好对应的目录(当然,这个不是必须的)
把之前的登录窗口放到对于View目录中,修改好对应的命名空间,这样,我们的View就差不多了,接下来看看我们的Model,它应该是什么呢?
在我们后台的WCF中,用户登录会到TB_Common_User表中进行验证,于是在我们的实体框架中就有了一个对应的实体类型,那么它是不是就是我们的Model呢?说实话,刚接触的时候我一直以为他们是一回事。但显然,他们不是同一个概念。做为实体的Model,它只是一个数据类型,方便我们在项目中使用,它本身不存在任何逻辑;而做为行为的Model,也就是我们LoginModel,它的责任就要大的多,它负责为ViewModel提供数据,并处理来自ViewModel中的反馈与请求,可以说,它是整个功能的核心要素,想想看,不管是MVC、MVP还是MVVM,为什么它总在第一位呢?呵呵。
知道了Model,那么VM又是什么呢?一个简单的理解:理论上讲,对于后台程序员来说,并不知道View的存在!因为在开发的过程中,View可能会由美工与前端程序员负责,而后台的业务逻辑等由后台程序员负责,为了并行开发和避免相互依赖,需要一个中间件来连接前后台,这就是ViewModel。因此,对于我们来说,ViewModel就是我们的View。至于它如何与View进行关联,这就是SL与WPF中的强大的Binding系统负责的事情了。
由此可以想像,为了“不知道”View存在,就需要在ViewModel中建立View需要的所有数据,我们的目的就是要“以数据驱动界面”,可以设想一下,界面上任何需要后台控制的属性都需要在ViewModel中有与之对应的属性存在,事件也是一样,可以通过命令进行绑定(不过似乎SL中对命令的支持有限,只有一系列按钮可以支持)。
有了上面的概念,回到我们的需求中,分析一下登录模块需要的功能流程:输入,验证输入,登录请求,登录验证,返回登录结果,下一步操作(成功则跳转,失败则提示)。对号入座:输入(View),验证输入(ViewModel),登录请求(Model),登录验证(WCF),返回登录结果(Model),下一步操作(ViewModel),责任分清之后可以进行实现了,不过这里有必要提一下验证输入这一块,在SL是可没有类似Web中那一系列验证控件(Tookit中有,但原理完全不同),SL中的验证可以通过IDataErrorInfo接口来实现,这个一会再说,先看一下代码吧,有时代码比文字更能说明问题。
public class LoginModel
{
WcfServiceClient client = new WcfServiceClient();
public string UserName { set; get; }
public string PassWord { set; get; }
public LoginModel()
{
client.loginCompleted += new EventHandler<loginCompletedEventArgs>(client_loginCompleted);
}
void client_loginCompleted(object sender, loginCompletedEventArgs e)
{
Action<int> callback = e.UserState as Action<int>;
if (callback != null)
callback(e.Result);
}
public void RequestLogin(Action<int> callback)
{
client.loginAsync(UserName, PassWord, callback);
}
}
LoginModel类包括两个属性与一个方法,RequestLogin的参数是一个Action,用来在验证登录后通知ViewModel做处理。再看看ViewModel的代码
public class LoginViewModel : INotifyPropertyChanged, IDataErrorInfo
{
public event PropertyChangedEventHandler PropertyChanged;
private Model.LoginModel lm = new Model.LoginModel();
public string UserName
{
get { return lm.UserName; }
set
{
lm.UserName = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("UserName"));
}
}
public string Password
{
get { return lm.PassWord; }
set
{
lm.PassWord = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Password"));
}
}
private ICommand _RequestLoginCommand;
public ICommand RequestLoginCommand
{
get
{
if (_RequestLoginCommand == null)
_RequestLoginCommand = new Commands.DelegateCommand(RequestLogin);
return _RequestLoginCommand;
}
}
public void RequestLogin(object obj)
{
lm.RequestLogin(RequestLoginCallBack);
}
private void RequestLoginCallBack(int loginstate)
{
if (loginstate == 1)
{
View.Login l = App.Current.RootVisual as View.Login;
l.Content = new MainPage();
}
}
#region IDataErrorInfo 成员
public string Error
{
get { return "出错啦!"; }
}
public string this[string columnName]
{
get
{
string result = null;
int len = 0;
switch (columnName)
{
case "UserName":
// 设置Username属性的验证规则
len = UserName.Length;
if (len < 2 || len > 10)
{
result = "Username length must between 2 and 10";
}
break;
case "Password":
// 设置Pwd属性的验证规则
len = Password.Length;
if (len < 2 || len > 10)
{
result = "Pwd length must between 2 and 10";
}
break;
}
return result;
}
}
#endregion
}
初一看好像代码很多,其实仔细看看的话,发现代码很简单。不过说实话,使用MVVM开发模式的话,一定会比平常的代码量要大的多,可是哪种模式不是这样呢?开发模式的优势不能简单的用纯代码量来衡量的。
<TextBlock HorizontalAlignment="Right" Margin="0" TextWrapping="Wrap" Text="密码:" Foreground="#FFFDFBFB" Width="{Binding Width, ElementName=textBlock}" TextAlignment="Right" VerticalAlignment="Center"/>
<PasswordBox HorizontalAlignment="Left" Width="{Binding Width, ElementName=txtUserName}" Height="28" VerticalAlignment="Center" Margin="0" x:Name="txtPassWord" Password="{Binding Password, Mode=TwoWay}" />
<Button Content="登录" HorizontalAlignment="Left" Height="27" Margin="0,0,8,0" Width="51" x:Name="btnLogin" Command="{Binding RequestLoginCommand}" />
这个LoginViewModel实现了INotifyPropertyChanged接口,用来双向绑定数据;实现了IDataErrorInfo接口用来验证输入(关于这两个接口的说明,请参考MSDN或其它详细说明)。这样,我们的VM就建立完成了,剩下的工作只要在view进行绑定就可以了,绑定如下
在初始化时实例化一个LoginViewModel并赋值到View的DataContext就完成了。
ViewModel.LoginViewModel lvm = new ViewModel.LoginViewModel();
this.DataContext = lvm;
实现效果如
以上就是登录功能的MVVM实现。代码很好理解,最主要的工作其实就是分清责任,有两个基本思想,就是
1、 Model是功能的核心,负责业务逻辑的实现。
2、 ViewModel负责为View提供数据,对Model来说,它就是所有的前台。
这就是我对MVVM的理解了,不知道是否正确,欢迎各个大牛指点拍砖!
最后,这里有两个小问题,当我们第一次打个这个窗口时,我们希望用户名输入框获得焦点,但默认情况下不行,需要做一些小处理,即在Load事件中设置一下
void Login_Loaded(object sender, RoutedEventArgs e)
{
HtmlPage.Plugin.Focus();
this.txtUserName.Focus();
}
然后,当载入完成后,我们还没有输入时,因为不满足数据验证条件,它也会在加载完就给出错误提示,这也不太好,至少你得先让我输入之后再验证吧。这里我找了一个取巧的办法,我发现Click事件会在绑定的Command执行前被触发,因此,我把绑定的动作放到第一次点击登录按钮时,如
void btnLogin_Click(object sender, RoutedEventArgs e)
{
if (this.DataContext == null)
{
ViewModel.LoginViewModel lvm = new ViewModel.LoginViewModel();
lvm.UserName = this.txtUserName.Text;
lvm.Password = this.txtPassWord.Password;
this.DataContext = lvm;
}
}
注意,在第一次绑定时,数据是会从数据源流向目标的,也就是说,我们需要在设置DataContext之前把输入的数据先设置到数据源中,否则如果没有lvm.UserName = this.txtUserName.Text;lvm.Password = this.txtPassWord.Password;这两句,在绑定时输入框就会被清空了。