Introduction
One of the challenges software developers face all the time is keeping up with the rapid pace of new technologies being released into the marketplace. This pace has quadrupled since the release of Microsoft .NET 1.0 and other related technologies several years ago.
With so many different pieces of technologies to choose from, it has become hard to determine which technology to invest your time in. I always ask myself, which technologies will take off, and which technologies will fade. We certainly have a lot to choose from. We have Design Patterns, and methodologies such as MVC, TDD, and Agile Development. We have technologies such as .NET 3.5, WPF, WCF, WF, LINQ, Silverlight, Entity Frameworks, and several other various technologies for both web based applications and Smart Client/desktop applications.
With ASP.NET MVC and Silverlight still in their early stages, I decided to focus on XAML and Windows Presentation Foundation. XAML and WPF are clearly going to be the next generation of tools for GUI development. But, that’s just my opinion.
Sample Application
The sample application for this article is a WPF application that uses Routed UI Commands. Routed UI Commands are a great technology advancement for those interested in developing applications that are much more testable using testing tools such as NUnit and MBUnit. Using Routed UI Commands also allows the developer to separate the GUI aspects of the application from the code-behind, thus allowing for and facilitating automated Unit Testing.
Using the Code
The WPF form for this sample application will allow you to scramble and de-scramble text. Additional buttons also exist to allow you to copy and paste the scrambled text to and from the clipboard. The scramble/descramble methods were borrowed from a Web Service I created several years ago when I first delved into building and consuming ASP.NET Web Services.
As you can see, the code-behind file for the XAML form does not contain any handlers for the six button clicks on the form. Using the RoutedUICommand
, these events have been routed to a separate class controller. The code-behind logic has thus been separated from the GUI implementation.
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
{
public partial class MessageView : Window
{
public MessageView()
{
InitializeComponent();
}
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;
}
}
}
The controller class now handles all the button click events through the _Executed
event handlers for each button. Now that I can handle the button clicks in a separate class, the next challenge for this sample application is to try and figure out how to update the user interface through the controller class.
After searching everywhere on the Internet for a working example of how to do this, and without much success, I accidentally moused over the sender
object parameter of the _Executed
event methods in debug mode and noticed that the object contained the entire contents of the XAML tree, including all the labels, textboxes, and buttons.
Using the FindName
method of the Window
object, I was able to access and update the controls on the form.
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;
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;
}
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)));
}
public void ScrambleCommand_CanExecute(Object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
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;
}
public void UnScrambleCommand_CanExecute(Object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
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;
}
public void ClearTopCommand_CanExecute(Object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
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;
}
public void ClearBottomCommand_CanExecute(Object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
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;
}
public void CopyToClipboardCommand_CanExecute(Object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
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;
Clipboard.SetData(DataFormats.Text, (Object)clipboardText);
appWindow.Cursor = Cursors.Arrow;
_messageBox.Show("The text has been copied to the clipboard");
}
public void PasteFromClipboardCommand_CanExecute(Object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
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;
}
}
public class CustomMessageBox
{
public virtual void Show(string message)
{
MessageBox.Show(message);
}
}
}
Testing the application now comes down to choosing an automated testing tool. I chose MBUnit to test the GUI over using NUnit because it has better support for Single-Threaded Applications, which WPF requires. Adding [TestFixture(ApartmentState = ApartmentState.STA)]
to the UnitTests
class did the trick.
For VB.NET developers, the setting for this is:
<TestFixture(""test", ApartmentState:=ApartmentState.STA)> _
The :=
syntax was a nightmare to find on the Internet.
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
{
internal class MyListener : TraceListener
{
public override void Write(string message)
{
Console.Write(message);
}
public override void WriteLine(string message)
{
Console.WriteLine(message);
}
}
[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;
[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();
}
[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==");
}
[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");
}
[Test()]
public void TestClearTopButton()
{
ICommand command = controller.ClearTopCommand;
command.Execute(null);
Assert.IsEmpty(txtNormalText.Text, txtNormalText.Text);
}
[Test()]
public void TestClearBottomButton()
{
ICommand command = controller.ClearBottomCommand;
command.Execute(null);
Assert.IsEmpty(txtScrambledText.Text, txtScrambledText.Text);
}
[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");
}
[Test()]
public void TestPasteButton()
{
ICommand command = controller.PasteFromClipboardCommand;
this.txtScrambledText.Text = "";
string clipboardText = "Mark Caplin";
Clipboard.SetData(DataFormats.Text, (Object)clipboardText);
command.Execute(null);
Assert.AreEqual(this.txtScrambledText.Text, "Mark Caplin");
}
[TearDown()]
public void CloseTestWindow()
{
appWindow.Close();
}
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
{
public override void Show(string message)
{
}
}
}
The test class I created essentially builds an empty test GUI using a StringBuilder
.
The test class also dynamically adds the controls to the XAML for testing the application. Reusing the loose XAML file from the sample application could have been another approach for testing the controller. As one of my goals, I wanted to learn how to dynamically add and register controls onto a XAML form.
In my Unit Tests, I simulate button clicks by performing the Execute
method of the ICommand
interface.
When initially testing my application through MBUnit, I noticed that the CopyToClipboardCommand
popped-up a message box alert saying that the contents have been copied to the clipboard. In a continuous integration environment where your Unit Tests run automatically without user intervention, I decided I needed to figure out a way to suppress the message box dialog from appearing in my test class.
While trying to research ways to mock the MessageBox
in my application, I decided to just implement a simple custom messagebox class and encapsulate the actual call to the Messagebox.Show
command in my own custom class. By doing so, I was able to override the Show
method of my CustomMessageBox
and suppress the dialog from appearing when running my automated Unit Tests.
Conclusion
One of the things I’ve learned about WPF is that there are several ways to skin a cat for any particular thing you are trying to accomplish. This was my initial foray into WPF.
Moving forward, I’m sure that I will discover other and perhaps better solutions to this sample application. Basically, my goal was to create a WPF application, separate the code-behind from the GUI, update the GUI from a controller, simulate a variation of the Model-View-Controller design pattern, and be able to test the application through an automated testing tool.
WPF is a very exciting technology. I believe WPF will be the tool of choice for next generation graphical user interfaces.