这几天看了一些关于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;这两句,在绑定时输入框就会被清空了。