从一个登录页面浅淡MVVM(一)

Silverlight 使用的是 MVVM 模式,可是有多少人在使用 MVVM ?我自己就没有。

在这里,我以一个登录页面为例子,和大家分享讨论一下 Silverlight 开发的一些点滴,

包括 Validation、MVVM 等。

 

不过要说明的是,我并没有学习过官方关于 MVVM 的文档,这个例子我是从实例中进行重构,

有所感受而写出来的,还是希望各位权威大侠进行指正,最好是有官方的文档,感激万分。

 

这个例子采用的是 Silverlight + WCF ,不是 Silverlight + WCF RIA Services,

如果您不熟悉 Silverlight + WCF ,请先学习这方面的内容。

 

这个例子分成三个阶段,分别是

一、MV模式(Model-View模式)

二、MVVM模式(Model-View-ViewModel模式)

三、MVVM模式,并使用Command

 

附:VS2010源代码下载 https://files.cnblogs.com/Sunpire/MVVMTutorial.zip

 

第一阶段:MV模式(Model-View模式)

其实,能使用MV模式,说明已经是入门了 Silverlight 了,有多少从ASP.NET、从Windows Form

转过来的朋友,不懂得 Binding、不懂得 Validation ,苦苦想要从 UI 中去访问数据,去校验数据。

 

这个例子的 WCF 端很简单,就是校验用户名、密码和验证码

 

WCF中的User类
namespace MVVMTutorial.Web.Models
{
[DataContract]
public class User
{
[DataMember]
public string UserName { get; set; }

[DataMember]
public string Password { get; set; }
}
}

 

 很简单的按当前时间生成4位校验码,用户名为不区分大小的test、密码为小写的test时登录成功

LoginService.svc
namespace MVVMTutorial.Web
{
[ServiceContract(Namespace
= "")]
[AspNetCompatibilityRequirements(RequirementsMode
= AspNetCompatibilityRequirementsMode.Allowed)]
public class LoginService
{
/// <summary>
/// 简单的记录校验码,为简单起见,认为校验码永久有效
/// </summary>
private static List<string> validationCodes = new List<string>();

/// <summary>
/// 生成简单的校验码
/// </summary>
/// <returns></returns>
[OperationContract]
public string GenerateValidationCode()
{
string ticks = DateTime.Now.Ticks.ToString();
System.Text.StringBuilder sb
= new System.Text.StringBuilder();
for (int i = ticks.Length - 5; i >= ticks.Length - 8; i--)
{
sb.Append(ticks[i]);
}
if (!validationCodes.Contains(sb.ToString()))
{
validationCodes.Add(sb.ToString());
}
return sb.ToString();
}

/// <summary>
/// 简单登录判断
/// </summary>
/// <param name="usr"></param>
/// <param name="validationCode"></param>
/// <returns>1:用户名密码正确;0:用户名密码不正确;2:校验码无效</returns>
[OperationContract]
public short Login(Models.User usr , string validationCode)
{
if (!validationCodes.Contains(validationCode))
{
return 2;
}
if (usr != null && usr.UserName.ToLower() == "test" && usr.Password == "test")
{
return 1;
}
else
{
return 0;
}
}
}
}

接下来就是 Silverlight 端,生成 LoginService.svc 的代理类

在代理 LoginProxy 的 Reference.cs 中,会生成 User 类的代码,

我会将其 Copy 出来,单独放在一个低层次的类库项目中,如上图的 Models 项目的 User.cs 文件,

为什么要这样做? 这是因为 Validation 的需要。

 

Validation 需要使用到 System.ComponentModel.DataAnnotations 中的各种 Attribute,

如 UserName 的写法是: 

User.cs 中的 UserName
[Required]
[StringLength(
6, MinimumLength = 3)]
[Display(Name
= "用户名", Description = "不区分大小写")]
[RegularExpression(
"^[a-zA-Z0-9\\-_.]*$", ErrorMessage = "只能输入字母、数字-_.")]
[System.Runtime.Serialization.DataMemberAttribute()]
public string UserName
{
get
{
return this.UserNameField;
}
set
{
if ((object.ReferenceEquals(this.UserNameField, value) != true))
{
this.ValidateProperty("UserName", value);
this.UserNameField = value;
this.RaisePropertyChanged("UserName");
}
}
}

 其中校验了 必填、长度为3-6字符、只能输入字母和数字 等,并且在前台UI的 Label 标签中显示为“用户名”等。

 PS:关于 Silverlight Validation,可以参考 jv9 的博客http://www.cnblogs.com/jv9/archive/2010/09/27/1836394.html

 

将 User 的 namespace 改为和 WCF 中的 User 的相同,生成 Models 项目,然后再更新 LoginProxy,

这样 Reference.cs 中不再含有对 User 的定义了。

 

Models 项目中存放的就是各种模型 Model,在实际项目中,绝对大数的 Model 来自于 WCF 中所使用到的

Entity,在这个例子中,还针对 校验码 增加了一个 ValidationModel ,用于表示校验码模型,这个模型只有

一种逻辑“校验码要和从WCF中取得的校验码相同”。 实际上,在WCF端并没有这个 Model,这里加上这个

Model,也是为了在 UI 应用 Binding 和 Validation。

 

接下来看 View 部分:

 

LoginPage.xaml
<sdk:Label Height="28" Margin="8" Width="120" Target="{Binding ElementName=txtUserName}" />
<TextBox Height="23" Margin="8" Width="120" Grid.Column="1" Name="txtUserName"
Text
="{Binding UserName,Mode=TwoWay,NotifyOnValidationError=True,ValidatesOnExceptions=True}" />

<sdk:Label Height="28" Margin="8" Width="120" Grid.Row="1" Target="{Binding ElementName=txtPassword}" />
<PasswordBox Height="23" Margin="8" Width="120" Grid.Row="1" Grid.Column="1" Name="txtPassword"
Password
="{Binding Password,Mode=TwoWay,NotifyOnValidationError=True,ValidatesOnExceptions=True}" />

<sdk:Label Height="28" Margin="8" Width="120" Grid.Row="2"
Target
="{Binding ElementName=txtValidationCode}" Name="lbValidationCode" />
<TextBox Height="23" Margin="8" Width="120" Grid.Row="2" Grid.Column="1" Name="txtValidationCode"
Text
="{Binding Code,Mode=TwoWay,NotifyOnValidationError=True,ValidatesOnExceptions=True}" />
<TextBlock Grid.Column="2" Grid.Row="2" Height="23" Margin="8" Name="tbValidationCode"
Text
="{Binding GivenCode,Mode=OneWay}" ToolTipService.ToolTip="请按此输入校验码" />
<Button Content="换一个" Grid.Column="3" Grid.Row="2" Height="23" Margin="8"
Name
="btnChangeValidationCode" Width="75" Click="btnChangeValidationCode_Click" />

<Button Content="登录" Grid.Row="3" Grid.ColumnSpan="4" Margin="8" Name="btnLogin"
Click
="btnLogin_Click" />

<sdk:ValidationSummary Grid.Row="3" Grid.ColumnSpan="4" Margin="8" Name="validationSummary1" />

   

这里使用了 Binding 和 ValidationSummary,个人认为这是使用 Silverlight 入门的标志:)

下图是运时的效果

接下来是 LoginPage.cs 的全部代码,代码较多,这是因为没有使用 ViewModel 这一层嘛,哈哈。 

LoginPage.cs中的事件响应代码
public partial class LoginPage : Page
{
User usr;
ValidationModel validationModel;
LoginProxy.LoginServiceClient client;

...

void LoginPage_Loaded(object sender, RoutedEventArgs e)
{
this.usr = new User();
this.validationModel = new ValidationModel();
this.DataContext = this.usr;
this.lbValidationCode.DataContext=this.tbValidationCode.DataContext
= this.txtValidationCode.DataContext = this.validationModel;

if (this.NeedInitializeClient())
{
this.InitializeClient();
}
this.client.GenerateValidationCodeAsync();
}

private void btnChangeValidationCode_Click(object sender, RoutedEventArgs e)
{
if (this.NeedInitializeClient())
{
this.InitializeClient();
}
this.client.GenerateValidationCodeAsync();
}

private void btnLogin_Click(object sender, RoutedEventArgs e)
{
if (this.validationSummary1.HasErrors)
{
this.validationSummary1.Focus();
return;
}
else
{
// 扩展方法,校验各个控件的数据绑定
this.LayoutRoot.Children.ValidateSource();
if (this.validationSummary1.HasErrors)
{
this.validationSummary1.Focus();
return;
}
}

if (this.NeedInitializeClient())
{
this.InitializeClient();
}
this.client.LoginAsync(this.usr , this.validationModel.Code);
}
  ...
}

使用了 User、ValidationModel 、LoginProxy.LoginServiceClient 三个实例,并且将前两者的实例作为

前台 UI 的 DataContext。

要指出的是在 btnLogin_Click() 有一段校验判断代码显得很臃肿,这是很无奈的,

Silverlight Validation机制是好,可是在UI的输入框不输入的情况下,点击[登录]按钮时,

this.validationSummary1是得不到任何校验的反馈的,this.validationSummary1.HasErrors == false,

此时只有手动的对各个控件的 Binding 进行验证,为此写了一个扩展方法:

ValidateSource()扩展方法
/// <summary>
/// 对集合中的 TextBox 等调用UpdateSource()方法,以校验数据源
/// </summary>
/// <param name="uiCollection"></param>
public static void ValidateSource(this UIElementCollection uiCollection)
{
foreach (UIElement uie in uiCollection)
{
if (uie is TextBox)
{
((TextBox)uie).ValidateSource();
}
else if (uie is PasswordBox)
{
((PasswordBox)uie).ValidateSource();
}
}
}
/// <summary>
///
/// </summary>
/// <param name="textBox"></param>
public static void ValidateSource(this TextBox textBox)
{
System.Windows.Data.BindingExpression binding
= textBox.GetBindingExpression(TextBox.TextProperty);
if (binding != null)
{
binding.UpdateSource();
}
}
/// <summary>
///
/// </summary>
/// <param name="pwdBox"></param>
public static void ValidateSource(this PasswordBox pwdBox)
{
System.Windows.Data.BindingExpression binding
= pwdBox.GetBindingExpression(PasswordBox.PasswordProperty);
if (binding != null)
{
binding.UpdateSource();
}
}

 

 

另外,在对 LoginProxy.LoginServiceClient 实例的使用上,每次WCF异步请求开始前都判断是否要初始化

WCF 代理,以防止页面在长时间搁置不动之后,WCF代理实例在达到超时时间后自动关闭,再次通过该实例

发送请求时便会出现 NOT FOUND 错误。

NeedInitializeClient
/// <summary>
/// 检查是否要初始化代理
/// </summary>
/// <returns></returns>
bool NeedInitializeClient()
{
return this.client == null ||
this.client.State == System.ServiceModel.CommunicationState.Closed ||
this.client.State == System.ServiceModel.CommunicationState.Faulted;
}

如果认为这是一个好习惯,可以将其写成扩展方法,如

NeedInitializeClient
/// <summary>
/// 是否需要初始化
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="client"></param>
public static bool NeedInitializeClient<T>(this System.ServiceModel.ClientBase<T> client) where T : class
{
if (client == null || client.State == System.ServiceModel.CommunicationState.Closed
|| client.State == System.ServiceModel.CommunicationState.Faulted)
{
return true;
}
else
{
return false;
}
}

 

下面是 WCF 异步请求完成的处理代码

InitializeClient()
/// <summary>
/// 初始化代理
/// </summary>
void InitializeClient()
{
// 使用默认的 Binding 和 EndpointAddress 初始化,在实际项目应使用
// new LoginProxy.LoginServiceClient(binding,remoteAddress)
this.client = new LoginProxy.LoginServiceClient();

this.client.GenerateValidationCodeCompleted += (sender, e) =>
{
if (e.Error == null)
{
this.validationModel.GivenCode = e.Result;
}
else
{
// 处理异常
MessageBox.Show(e.Error.Message);
}
};

this.client.LoginCompleted += (sender, e) =>
{
if (e.Error == null)
{
if (e.Result == 1)
{
// 登录成功
MessageBox.Show("登录成功");
}
else if (e.Result == 0)
{
// 用户名或密码错误
MessageBox.Show("用户名或密码错误");
}
else if (e.Result == 2 )
{
// 校验码失效
MessageBox.Show("校验码失效");
}
}
else
{
// 处理异常
MessageBox.Show(e.Error.Message);
}
};
}

简单起见都是使用MessageBox.Show()来处理异常或提示消息。

 

 

 

至此,这个例子的第一阶段完成了,这是一个完整的例子,

LogingPage.cs 中的代码也是相当清晰的了,

可是 LogingPage.cs 中必竟还是混杂了一些控制逻辑,这也导致了要进一步分离这些代码的需要,

这便是 ViewModel 层次。

 

二、MVVM模式(Model-View-ViewModel模式)

 

posted @ 2010-12-25 21:10  Sunpire  阅读(6168)  评论(5编辑  收藏  举报