Click here to Skip to main content
Click here to Skip to main content

Developing and Testing a WPF Application Using Routed UI Commands and MbUnit

, 26 Dec 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
Developing and testing a WPF application using Routed UI Commands and MbUnit.

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.

Application.JPG

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
{
    /// <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;
        }
    }
}

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;
        /// <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);            
        }
    }
}

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
{
    /// <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
        }
    }
}

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.

messagebox.JPG

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.

MBUnit.JPG

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Mark J. Caplin
Software Developer Caplin Systems
United States United States
Mark Caplin has specialized in Information Technology solutions for 25 years. Specializing in full life-cycle development projects for both enterprise-wide systems and Internet/Intranet based solutions.
 
For the past ten years or so, Mark has specialized in the Microsoft .NET framework using both C# and VB.NET as his tools of choice.
 
Mark currently works for e-Builder Inc, www.e-builder.net. e-Builder is a SaaS software company specializing in Construction Program Management Software. If you are a talented Microsoft .NET developer and you are looking for a new opportunity with a rapidly growing company, please send me your resume.
 
When not coding, Mark enjoys playing tennis, listening to U2 music, watching Miami Dolphins football and watching movies in Blu-Ray technology.
 
In between all this, his wife of over 20 years, feeds him well with some great home cooked meals.
 
...

Comments and Discussions

 
GeneralMy Vote 5 PinmemberShemeemsha RA2-Oct-14 20:22 
GeneralWe can bind commands in XAML Pinmembertime-best7-Jan-09 19:57 
GeneralRe: We can bind commands in XAML PinmemberMark J. Caplin8-Jan-09 4:54 
GeneralMbUnit v3 PinmemberRichard Cunday27-Dec-08 8:23 
GeneralRe: MbUnit v3 PinmemberMark J. Caplin29-Dec-08 18:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141220.1 | Last Updated 26 Dec 2008
Article Copyright 2008 by Mark J. Caplin
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid