使用路由 UI 命令和 MbUnit 开发和测试 WPF 应用程序






4.60/5 (3投票s)
使用路由 UI 命令和 MbUnit 开发和测试 WPF 应用程序。
引言
软件开发人员一直面临的挑战之一是跟上市场上新技术的快速发展。自几年前 Microsoft .NET 1.0 及相关技术发布以来,这种速度已翻倍。
有如此多的技术可供选择,很难确定应该投资哪种技术。我总是问自己,哪些技术会流行,哪些技术会消退。我们确实有很多选择。我们有设计模式和方法论,例如 MVC、TDD 和敏捷开发。我们有 .NET 3.5、WPF、WCF、WF、LINQ、Silverlight、Entity Frameworks 等技术,以及用于 Web 应用程序和智能客户端/桌面应用程序的各种其他技术。
随着 ASP.NET MVC 和 Silverlight 仍处于早期阶段,我决定专注于 XAML 和 Windows Presentation Foundation。XAML 和 WPF 显然将成为下一代 GUI 开发工具。但这只是我的看法。
示例应用
本文的示例应用程序是一个使用路由 UI 命令的 WPF 应用程序。路由 UI 命令是一项很棒的技术进步,对于那些有兴趣开发更易于使用 NUnit 和 MBUnit 等测试工具进行测试的应用程序的人来说。使用路由 UI 命令还可以让开发人员将应用程序的 GUI 部分与代码隐藏分离,从而实现并促进自动单元测试。
Using the Code
此示例应用程序的 WPF 表单允许您打乱和恢复文本。此外还有其他按钮允许您将打乱的文本复制到剪贴板和从剪贴板粘贴。打乱/恢复方法借鉴了我几年前在首次深入构建和使用 ASP.NET Web 服务时创建的 Web 服务。
如您所见,XAML 表单的代码隐藏文件不包含对表单上六个按钮单击的任何处理程序。使用 RoutedUICommand
,这些事件已路由到一个单独的类控制器。因此,代码隐藏逻辑已与 GUI 实现分离。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using WpfMessageController;
using System.IO;
using System.Net;
using System.Windows.Media.Animation;
using System.Collections.ObjectModel;
namespace WpfMessage
{
/// <summary>
/// Interaction logic for MessageView.xaml
/// </summary>
public partial class MessageView : Window
{
public MessageView()
{
InitializeComponent();
}
/// <summary>
/// Window Loaded
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Window_Loaded(object sender, RoutedEventArgs e)
{
WpfMessageController.Controller controller;
WpfMessageController.CustomMessageBox messageBox;
messageBox = new WpfMessageController.CustomMessageBox();
controller = new WpfMessageController.Controller(messageBox);
controller.BindCommandsToWindow(this);
this.btnScramble.Command = controller.ScrambleCommand;
this.btnUnScramble.Command = controller.UnScrambleCommand;
this.btnClearTop.Command = controller.ClearTopCommand;
this.btnClearBottom.Command = controller.ClearBottomCommand;
this.btnCopyToClipBoard.Command = controller.CopyToClipboardCommand;
this.btnPasteFromClipBoard.Command = controller.PasteFromClipboardCommand;
}
}
}
控制器类现在通过每个按钮的 _Executed
事件处理程序处理所有按钮单击事件。既然我可以在一个单独的类中处理按钮单击,那么这个示例应用程序的下一个挑战是弄清楚如何通过控制器类更新用户界面。
在互联网上到处搜索一个可行的示例来做到这一点,但没有太多成功,我在调试模式下意外地将鼠标悬停在 _Executed
事件方法的 sender
对象参数上,并注意到该对象包含整个 XAML 树的内容,包括所有标签、文本框和按钮。
使用 Window
对象的 FindName
方法,我能够访问和更新表单上的控件。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Windows.Controls;
using System.Windows.Input;
using System.Windows.Controls;
using System.Windows;
namespace WpfMessageController
{
public class Controller
{
public RoutedUICommand ScrambleCommand;
public RoutedUICommand UnScrambleCommand;
public RoutedUICommand ClearTopCommand;
public RoutedUICommand ClearBottomCommand;
public RoutedUICommand CopyToClipboardCommand;
public RoutedUICommand PasteFromClipboardCommand;
public CustomMessageBox _messageBox;
/// <summary>
/// Controller Constructor
/// </summary>
public Controller(CustomMessageBox messageBox)
{
ScrambleCommand = new RoutedUICommand("ScrambleCommand",
"ScrambleCommand", typeof(Controller));
UnScrambleCommand = new RoutedUICommand("UnScrambleCommand",
"UnScrambleCommand", typeof(Controller));
ClearTopCommand = new RoutedUICommand("ClearTopCommand",
"ClearTopCommand", typeof(Controller));
ClearBottomCommand = new RoutedUICommand("ClearBottomCommand",
"ClearBottomCommand", typeof(Controller));
CopyToClipboardCommand = new RoutedUICommand("CopyToClipboardCommand",
"CopyToClipboardCommand", typeof(Controller));
PasteFromClipboardCommand = new RoutedUICommand("PasteFromClipboardCommand",
"PasteFromClipboardCommand", typeof(Controller));
_messageBox = messageBox;
}
/// <summary>
/// Bind Commands
/// </summary>
/// <param name="oWindow"></param>
public void BindCommandsToWindow(Window appWindow)
{
appWindow.CommandBindings.Add(new CommandBinding(ScrambleCommand,
new ExecutedRoutedEventHandler(ScrambleCommand_Executed),
new CanExecuteRoutedEventHandler(ScrambleCommand_CanExecute)));
appWindow.CommandBindings.Add(new CommandBinding(UnScrambleCommand,
new ExecutedRoutedEventHandler(UnScrambleCommand_Executed),
new CanExecuteRoutedEventHandler(UnScrambleCommand_CanExecute)));
appWindow.CommandBindings.Add(new CommandBinding(ClearTopCommand,
new ExecutedRoutedEventHandler(ClearTopCommand_Executed),
new CanExecuteRoutedEventHandler(ClearTopCommand_CanExecute)));
appWindow.CommandBindings.Add(new CommandBinding(ClearBottomCommand,
new ExecutedRoutedEventHandler(ClearBottomCommand_Executed),
new CanExecuteRoutedEventHandler(ClearBottomCommand_CanExecute)));
appWindow.CommandBindings.Add(new CommandBinding(CopyToClipboardCommand,
new ExecutedRoutedEventHandler(CopyToClipboardCommand_Executed),
new CanExecuteRoutedEventHandler(CopyToClipboardCommand_CanExecute)));
appWindow.CommandBindings.Add(new CommandBinding(PasteFromClipboardCommand,
new ExecutedRoutedEventHandler(PasteFromClipboardCommand_Executed),
new CanExecuteRoutedEventHandler(PasteFromClipboardCommand_CanExecute)));
}
/// <summary>
/// Scramble Button Can Execute
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void ScrambleCommand_CanExecute(Object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
/// <summary>
/// Scramble Command Executed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void ScrambleCommand_Executed(Object sender, ExecutedRoutedEventArgs e)
{
Window appWindow;
TextBox txtNormalText;
TextBox txtScrambledText;
string inputText;
string outputText;
appWindow = (Window)sender;
appWindow.Cursor = Cursors.Wait;
txtNormalText = (TextBox)appWindow.FindName("txtNormalText");
inputText = txtNormalText.Text;
outputText = "";
MessageSecurity oSecurity = new MessageSecurity();
outputText = oSecurity.EncryptText(inputText);
txtScrambledText = (TextBox)appWindow.FindName("txtScrambledText");
txtScrambledText.Text = outputText;
appWindow.Cursor = Cursors.Arrow;
}
/// <summary>
/// Unscramble Command Can Execute
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void UnScrambleCommand_CanExecute(Object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
/// <summary>
/// Unscramble Command Executed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void UnScrambleCommand_Executed(Object sender, ExecutedRoutedEventArgs e)
{
Window appWindow;
TextBox txtNormalText;
TextBox txtScrambledText;
string inputText;
string outputText;
appWindow = (Window)sender;
appWindow.Cursor = Cursors.Wait;
txtNormalText = (TextBox)appWindow.FindName("txtNormalText");
txtScrambledText = (TextBox)appWindow.FindName("txtScrambledText");
inputText = txtScrambledText.Text;
outputText = "";
MessageSecurity oSecurity = new MessageSecurity();
outputText = oSecurity.DecryptText(inputText);
txtNormalText.Text = outputText;
appWindow.Cursor = Cursors.Arrow;
}
/// <summary>
/// Clear Top Command Can Execute
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void ClearTopCommand_CanExecute(Object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
/// <summary>
/// Clear Top Command Executed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void ClearTopCommand_Executed(Object sender, ExecutedRoutedEventArgs e)
{
Window appWindow;
TextBox txtNormalText;
appWindow = (Window)sender;
appWindow.Cursor = Cursors.Wait;
txtNormalText = (TextBox)appWindow.FindName("txtNormalText");
txtNormalText.Text = "";
appWindow.Cursor = Cursors.Arrow;
}
/// <summary>
/// Clear Bottom Command Can Execute
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void ClearBottomCommand_CanExecute(Object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
/// <summary>
/// Clear Bottom Command Executed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void ClearBottomCommand_Executed(Object sender, ExecutedRoutedEventArgs e)
{
Window appWindow;
TextBox txtScrambledText;
appWindow = (Window)sender;
appWindow.Cursor = Cursors.Wait;
txtScrambledText = (TextBox)appWindow.FindName("txtScrambledText");
txtScrambledText.Text = "";
appWindow.Cursor = Cursors.Arrow;
}
/// <summary>
/// Copy To Clipboard Command Can Execute
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void CopyToClipboardCommand_CanExecute(Object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
/// <summary>
/// Execute Copy To Clipboard Command
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void CopyToClipboardCommand_Executed(Object sender,
ExecutedRoutedEventArgs e)
{
Window appWindow;
TextBox txtScrambledText;
string clipboardText;
appWindow = (Window)sender;
appWindow.Cursor = Cursors.Wait;
txtScrambledText = (TextBox)appWindow.FindName("txtScrambledText");
clipboardText = txtScrambledText.Text;
// After this call, the data (string) is placed on the clipboard and tagged
// with a data format of "Text".
Clipboard.SetData(DataFormats.Text, (Object)clipboardText);
appWindow.Cursor = Cursors.Arrow;
_messageBox.Show("The text has been copied to the clipboard");
}
/// <summary>
/// Paste From Clipboard Command Can Execute
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void PasteFromClipboardCommand_CanExecute(Object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
/// <summary>
/// Execute Paste From Clipboard Command
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void PasteFromClipboardCommand_Executed(Object sender,
ExecutedRoutedEventArgs e)
{
Window appWindow;
TextBox txtScrambledText;
string clipboardText;
appWindow = (Window)sender;
appWindow.Cursor = Cursors.Wait;
txtScrambledText = (TextBox)appWindow.FindName("txtScrambledText");
clipboardText = Clipboard.GetText(TextDataFormat.Text);
txtScrambledText.Text = clipboardText;
appWindow.Cursor = Cursors.Arrow;
}
}
/// <summary>
/// Custom Message Box
/// </summary>
public class CustomMessageBox
{
public virtual void Show(string message)
{
MessageBox.Show(message);
}
}
}
现在,测试应用程序取决于选择一个自动化测试工具。我选择了 MBUnit 来测试 GUI,而不是使用 NUnit,因为它对 WPF 所需的单线程应用程序有更好的支持。将 [TestFixture(ApartmentState = ApartmentState.STA)]
添加到 UnitTests
类就解决了问题。
对于 VB.NET 开发人员,设置如下:
<TestFixture(""test", ApartmentState:=ApartmentState.STA)> _
:=
语法是在互联网上查找的噩梦。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Xml;
using System.IO;
using System.Windows.Controls;
using System.Windows;
using Microsoft.Windows.Controls;
using System.Windows.Markup;
using MbUnit.Framework;
using MbUnit.Core.Framework;
using System.Threading;
using System.Windows.Input;
using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;
namespace WpfMessageUnitTests
{
/// <summary>
/// Listener
/// </summary>
internal class MyListener : TraceListener
{
public override void Write(string message)
{
Console.Write(message);
}
public override void WriteLine(string message)
{
Console.WriteLine(message);
}
}
/// <summary>
/// Unit Tests
/// </summary>
[TestFixture(ApartmentState = ApartmentState.STA)]
public class UnitTests
{
private static MyListener listener = new MyListener();
Window appWindow;
WpfMessageController.Controller controller;
TextBox txtNormalText;
TextBox txtScrambledText;
Button btnClearTopButton;
Button btnClearBottomButton;
Button btnScrambleButton;
Button btnUnScrambleButton;
Button btnCopyButton;
Button btnPasteButton;
/// <summary>
/// Initialize Testing objects
/// </summary>
[SetUp()]
public void Init()
{
string xaml;
if ((!Trace.Listeners.Contains(listener)))
{
Trace.Listeners.Add(listener);
}
xaml = this.LoadXaml();
StringReader stringReader = new StringReader(xaml);
XmlReader xmlReader = XmlReader.Create(stringReader);
appWindow = (Window)XamlReader.Load(xmlReader);
IAddChild container;
WpfMessageUnitTests.TestMessageBox messageBox;
messageBox = new WpfMessageUnitTests.TestMessageBox();
controller = new WpfMessageController.Controller(messageBox);
controller. BindCommandsToWindow(appWindow);
this.btnClearTopButton = new Button();
this.btnClearTopButton.Name = "btnClearTopButton";
this.btnClearTopButton.Content = "Clear";
this.btnClearTopButton.Command = controller.ClearTopCommand;
this.btnClearBottomButton = new Button();
this.btnClearBottomButton.Name = "btnClearBottomButton";
this.btnClearBottomButton.Content = "Clear";
this.btnClearBottomButton.Command = controller.ClearBottomCommand;
this.btnScrambleButton = new Button();
this.btnScrambleButton.Name = "Scramble";
this.btnScrambleButton.Content = "Scramble";
this.btnScrambleButton.Command = controller.ScrambleCommand;
this.btnUnScrambleButton = new Button();
this.btnUnScrambleButton.Name = "UnSrcamble";
this.btnUnScrambleButton.Content = "Un-Scramble";
this.btnUnScrambleButton.Command = controller.UnScrambleCommand;
this.btnCopyButton = new Button();
this.btnCopyButton.Name = "Copy";
this.btnCopyButton.Content = "Copy";
this.btnCopyButton.Command = controller.CopyToClipboardCommand;
this.btnPasteButton = new Button();
this.btnPasteButton.Name = "Paste";
this.btnPasteButton.Content = "Paste";
this.btnPasteButton.Command = controller.PasteFromClipboardCommand;
this.txtNormalText = new TextBox();
this.txtNormalText.Name = "txtNormalText";
this.txtNormalText.Text = "not empty";
appWindow.RegisterName("txtNormalText", this.txtNormalText);
this.txtScrambledText = new TextBox();
this.txtScrambledText.Name = "txtScrambledText";
this.txtScrambledText.Text = "not empty";
appWindow.RegisterName("txtScrambledText",
this.txtScrambledText);
StackPanel stackPanel =
(StackPanel)appWindow.FindName("TestStackPanel");
container = stackPanel;
container.AddChild(this.txtNormalText);
container.AddChild(this.txtScrambledText);
container.AddChild(this.btnClearTopButton);
container.AddChild(this.btnClearBottomButton);
container.AddChild(this.btnScrambleButton);
container.AddChild(this.btnUnScrambleButton);
container.AddChild(this.btnCopyButton);
container.AddChild(this.btnPasteButton);
appWindow.Show();
}
/// <summary>
/// Test Scramble Button
/// </summary>
[Test()]
public void TestScrambleButton()
{
ICommand command = controller.ScrambleCommand;
this.txtNormalText.Text = "This is a sample " +
"application using Route UI Commands";
this.txtScrambledText.Text = "";
command.Execute(null);
Assert.AreEqual(this.txtScrambledText.Text,
"c7SIEdFn/Qf+0vGwuKhBIEuJQiAIIw/mUJZ/kw8LTKiLrl" +
"m9HFdT9txFZSJfLlKNyv2mFK8UjhsAmv9uEg2E7A==");
}
/// <summary>
/// Test Unscramble Button
/// </summary>
[Test()]
public void TestUnScrambleButton()
{
ICommand command = controller.UnScrambleCommand;
this.txtNormalText.Text = "";
this.txtScrambledText.Text = "c7SIEdFn/Qf+0vGwuKhBIEuJQiAIIw/" +
"mUJZ/kw8LTKiLrlm9HFdT9txFZSJfLlKNyv2mFK8UjhsAmv9uEg2E7A==";
command.Execute(null);
Assert.AreEqual(txtNormalText.Text,
"This is a sample application using Route UI Commands");
}
/// <summary>
/// Test Clear Top Button
/// </summary>
[Test()]
public void TestClearTopButton()
{
ICommand command = controller.ClearTopCommand;
command.Execute(null);
Assert.IsEmpty(txtNormalText.Text, txtNormalText.Text);
}
/// <summary>
/// Test Clear Bottom Button
/// </summary>
[Test()]
public void TestClearBottomButton()
{
ICommand command = controller.ClearBottomCommand;
command.Execute(null);
Assert.IsEmpty(txtScrambledText.Text, txtScrambledText.Text);
}
/// <summary>
/// Test Copy Button
/// </summary>
[Test()]
public void TestCopyButton()
{
ICommand command = controller.CopyToClipboardCommand;
this.txtScrambledText.Text = "This is a sample" +
" application using Route UI Commands";
command.Execute(null);
string clipboardText = Clipboard.GetText(TextDataFormat.Text);
Assert.AreEqual(clipboardText, "This is a sample application" +
" using Route UI Commands");
}
/// <summary>
/// Test Paste Button
/// </summary>
[Test()]
public void TestPasteButton()
{
ICommand command = controller.PasteFromClipboardCommand;
this.txtScrambledText.Text = "";
string clipboardText = "Mark Caplin";
// After this call, the data (string)
// is placed on the clipboard and tagged
// with a data format of "Text".
Clipboard.SetData(DataFormats.Text, (Object)clipboardText);
command.Execute(null);
Assert.AreEqual(this.txtScrambledText.Text, "Mark Caplin");
}
/// <summary>
/// Close Test Window
/// </summary>
[TearDown()]
public void CloseTestWindow()
{
appWindow.Close();
}
/// <summary>
/// Load Xaml
/// </summary>
/// <returns></returns>
private string LoadXaml()
{
StringBuilder xamlBuilder;
xamlBuilder = new StringBuilder();
xamlBuilder.Append("<Window");
xamlBuilder.Append(" xmlns='http://schemas.microsoft." +
"com/winfx/2006/xaml/presentation'");
xamlBuilder.Append(" Title='Hello World' Name='myTestWindow'>");
xamlBuilder.Append(" <StackPanel Name='TestStackPanel'>");
xamlBuilder.Append(" </StackPanel>");
xamlBuilder.Append(" </Window>");
return xamlBuilder.ToString();
}
}
internal class TestMessageBox : WpfMessageController.CustomMessageBox
{
/// <summary>
/// Override Show Message for testing
/// </summary>
/// <param name="message"></param>
public override void Show(string message)
{
// show nothing
}
}
}
我创建的测试类基本上使用 StringBuilder
构建了一个空的测试 GUI。
测试类还动态地将控件添加到 XAML 中以测试应用程序。重用示例应用程序中的松散 XAML 文件可能是测试控制器的另一种方法。作为我的目标之一,我想学习如何动态地将控件添加到 XAML 表单并注册它们。
在我的单元测试中,我通过执行 ICommand
接口的 Execute
方法来模拟按钮单击。
在最初通过 MBUnit 测试我的应用程序时,我注意到 CopyToClipboardCommand
弹出一个消息框提示,说明内容已复制到剪贴板。在没有用户干预的情况下自动运行单元测试的持续集成环境中,我决定需要找到一种方法来阻止消息框在我的测试类中出现。
在尝试研究如何在我的应用程序中模拟 MessageBox
的方法时,我决定实现一个简单的自定义消息框类,并将对 Messagebox.Show
命令的实际调用封装在我自己的自定义类中。通过这样做,我可以覆盖我的 CustomMessageBox
的 Show
方法,并在运行我的自动化单元测试时阻止对话框出现。
结论
我从 WPF 学到的一件事是,对于你试图完成的任何特定事情,都有几种方法可以实现。这是我最初尝试使用 WPF。
展望未来,我确信我将为这个示例应用程序发现其他,甚至可能更好的解决方案。基本上,我的目标是创建一个 WPF 应用程序,将代码隐藏与 GUI 分离,从控制器更新 GUI,模拟模型-视图-控制器设计模式的变体,并且能够通过自动化测试工具测试应用程序。
WPF 是一项令人兴奋的技术。我相信 WPF 将成为下一代图形用户界面的首选工具。