使用WPF、MVVM和TPL创建加密随机密码/数据集






4.33/5 (9投票s)
使用WPF和MVVM实现的随机数据/密码生成器,通过MVVM模式实现GUI逻辑和业务逻辑的清晰分离。
引言
本项目旨在演示如何正确生成随机密码(或大型随机数据集),并展示极其有用的模型-视图-视图模型(MVVM)架构。MVVM是一种分离关注点的模式,这意味着用户界面逻辑和业务逻辑不会交织在一起。相反,一个类(视图模型)位于UI代码(视图)和业务逻辑(模型)之间。WPF利用额外的功能,即数据绑定,来促进MVVM在WPF应用程序中的实现。
背景
为了理解本文的所有方面,读者最好对核心C#概念有深入的了解,包括集合类、线程同步以及委托和事件。对WPF和XAML有一定的了解也会有帮助。
为了加快开发速度,建议使用Ofir Shemesh的非常有用的**WPF MVVM项目模板**。可以通过Visual Studio 2010的扩展管理器获得。本项目使用的文件夹和代码结构是由WPF MVVM项目模板的3.0版本生成的。
Using the Code
本项目中的代码旨在尽可能自明,前提是读者对通用C#概念有深入的了解,并愿意学习WPF、XAML和MVVM。本文介绍的项目是独立的。下载源代码并运行,会生成一个如下所示的窗口。
项目文件夹和文件组织结构如下图所示。
MVVM组件概览
您会注意到有一个视图、一个视图模型和两个模型。`MainWindow`视图封装了XAML代码和代码隐藏(尽管后者基本为空,意味着所有视图渲染都通过标记完成)。`MainWindowViewModel`继承自一个简单的基类,该基类又继承自`NotificationObject`,以便于通知在视图和模型之间流动。两个模型类,`RandPasswordModel`和`StatsModel`,负责存储有关随机密码生成器设置、生成的随机数据以及从生成的密码/数据字符串中提取的统计信息的内存数据。这些统计信息可用于评估生成数据的随机性。
XAML
构造视图(同样,全部在XAML中)大量使用了设计器以及直接编辑XAML代码。您可以根据自己的偏好选择依赖设计器还是手动编码。
关键的是,您会注意到在XAML的窗口资源部分使用了视图模型和转换器(稍后将详细介绍转换器)。
<Window.Resources>
<localVM:MainWindowViewModel x:Key="Windows1ViewModel" />
<localConverters:CharArrayToStrConverter x:Key="CharArrayToStrConverter" />
<localConverters:StrListToStrConverter x:Key="StrListToStrConverter" />
<localConverters:DictionaryToText x:Key="DictionaryToText" />
</Window.Resources>
这些元素为视图提供了与视图模型以及稍后用于数据转换的转换器的链接,数据会在视图和模型之间流动。
将WPF控件绑定到数据和命令的能力使我们能够执行操作并接收数据。下图展示了将按钮点击与命令关联以及从业务层接收文本的XAML语法。
<Button Content="Create passwords" Height="23" Name="button1"
Width="233" HorizontalAlignment="Left" Command="{Binding GeneratePassesCommand}" />
...
<TextBox Height="300" Name="textBox3" Width="Auto"
HorizontalAlignment="Stretch" VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
Text="{Binding Model.GeneratedPass, Mode=OneWay, Converter={StaticResource StrListToStrConverter} }" />
在前面的示例中,您会注意到按钮与`GeneratePassesCommand`命令相关联。该命令流向视图模型中同名的`GeneratePasswords`方法,该方法调用模型中相应的`GeneratePasswords`方法。生成后,`TextBox`控件接收生成的随机数据集,将其转换为可绑定到`TextBox`控件的内容(通过`StrListToStrConverter`),然后渲染为控件中的文本。
视图模型 - 审视`GeneratePassesCommand`
`GeneratePassesCommand`的类型是`System.Windows.Input.ICommand`,声明如下:
public ICommand GeneratePassesCommand { get { return new DelegateCommand(GeneratePasswords); } }
`GeneratePassesCommand`封装了一个`Action`类型,在本例中,它调用私有的`GeneratePasswords`方法。请看下面的完整方法代码。
private void GeneratePasswords()
{
AppStatus = "Generating passwords...";
AsyncCallHelper.RunAync(() =>
{
List<string> passes = new List<string>(Model.NumPasses);
Parallel.For(0, Model.NumPasses, i =>
{
int seed = RandomGenHelper.GetCryptographicallyRandomInt32();
Random r = new Random(seed);
string pass = RandomGenHelper.GeneratePassword(r, Model.Length, Model.AllowableChars);
lock (passes)
{
passes.Add(pass);
}
} );
Model.GeneratedPass = passes;
StatsModel = new StatsModel(Model.GeneratedPass);
AppStatus = string.Format("{0} passwords generated", Model.NumPasses);
} );
}
回到视图,由于前面描述的`TextBox`控件绑定到模型中的`GeneratedPass`属性,生成的密码(或随机数据)在生成后,并且在执行属性设置器时(由这行代码调用:`Model.GeneratedPass = passes;`),会被馈送到控件中。
还值得一提的是辅助方法的用法,这些方法可以在其他地方使用,包括跨模型。例如,上面`GeneratePasswords()`方法中的整个代码块通过调用`RunAsync`来通过.NET`Task`执行。为了说明,让我们看一下那段代码。
public class AsyncCallHelper
{
static public void RunAync(Action action)
{
Task.Factory.StartNew(action);
}
}
事实上,整个类只有几行!异步辅助方法利用了非常强大的TPL(任务并行库)作为异步操作执行。通过使用这种技术,我们遵循了提高应用程序响应能力的建议指南,因为昂贵的业务逻辑的执行是在UI线程之外完成的。
模型 - 仔细查看`RandPasswordModel`
模型的`GeneratedPass`只是一个常规属性,有一个注意事项。一旦设置了该属性,必须发送通知,以便更新视图。
private List<string> _GeneratedPass;
public List<string> GeneratedPass
{
get { return this._GeneratedPass; }
set
{
if (this._GeneratedPass != value)
{
this._GeneratedPass = value;
RaisePropertyChanged(() => GeneratedPass);
}
}
}
请记住,本项目中两个模型的基类都继承自`NotificationObject`。一旦在模型属性的setter中调用了`RaisePropertyChanged`,就会触发`PropertyChangedEventHandler`事件,通知视图中(绑定的)控件属性已发生更改。
在离开这个主题之前,您会回忆起在上面的视图模型部分,我们深入研究了`GeneratePasswords`方法。如果您再次查看该方法,您会看到获取随机种子和随机数据是通过调用`RandomGenHelper`类中的辅助方法来完成的。让我们来看看这段代码并进行检查。
public static class RandomGenHelper
{
static public Int32 GetCryptographicallyRandomInt32()
{
byte[] randomBytes = new byte[4];
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
rng.GetBytes(randomBytes);
Int32 randomInt = BitConverter.ToInt32(randomBytes, 0);
return randomInt;
}
static public string GeneratePassword(Random r, int length, char[] allowableChars)
{
StringBuilder passwordBuilder = new StringBuilder((int)length);
for (int i = 0; i < length; i++)
{
int nextInt = r.Next(allowableChars.Length);
char c = allowableChars[nextInt];
passwordBuilder.Append(c);
}
return passwordBuilder.ToString();
}
}
上面,您将看到对`GetCryptographicallyRandomInt32`的调用。该方法利用了`System.Security.Cryptography`命名空间。在这里,我们使用了一个众所周知的库来帮助生成高度随机的32位整数。这些加密安全的随机数值种子被馈送到`Random`类,然后该类被上面的`GeneratePassword`方法使用,以生成随机密码(即,随机数据集),使用用户指定的字符集(并提供了一个合理的默认值)。**从业务逻辑的角度来看,这是代码的核心。**
转换器 - 何时以及如何使用它们
当来自模型或路由到模型的数据需要翻译才能被理解时,就需要使用转换器。对于`GeneratedPass`属性,我们可以使用一个简单的转换器将`List
public class StrListToStrConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null)
return string.Empty;
List<string> passes = (List<string>)value;
string ret=string.Join(Environment.NewLine, passes.ToArray<string>() );
return ret;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
这样,我们就完成了将字符串列表转换为常规字符串,以便能够绑定到`TextBox`控件。
结论
在本篇文章中,我们涵盖了在C# for .NET 4.0的WPF桌面应用程序的背景下实现MVVM模式的高级概念,以及辅助主题,包括用于并行工作和异步操作的任务并行库、用于命令委托的`ICommand`对象的使用,以及用于随机数播种的加密库的使用。
关注点
我非常喜欢编写这个应用程序来展示WPF与MVVM结合的强大功能和灵活性。我希望其他人也能发现它同样有用和鼓舞人心。
历史
版本 1.0 - 可运行的MVVM应用程序。