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

MVP-VM (Model View Presenter - ViewModel) with Data Binding and BackgroundWorker in Windows Forms applications

By , 17 Jun 2010
 

Introduction

Is your XxxForm.cs or .vb file cluttered with tons of event handlers that process your business logic? Even if you were able to separate out the domain logic from the UI, is your Form class file still contaminated with user input validation related code, or those visual controls' Enable/Disable, or Show/Hide controls to make your app just a little more user-friendly? If so, this article may help you sort them out, and you may be able to separate 90% of the stuff out of the form file. Some of the keywords that make it possible are Data Binding and the MVP-VM (Model View Presenter – ViewModel) pattern.

The above screenshot is the example application that I'll use in this article. Since I'm fed up with OOP tutorials that use “Employee”, “Customer”, or “Order” classes, I wanted something completely different, and decided to use a math problem that I took from the Project Euler site - http://projecteuler.net/. It solves the problem number 1 – “Add all the natural numbers below one thousand that are multiples of 3 or 5” in general form. The screenshot shows the correct answer, and you can get it by filling out two textboxes with your desired numbers and clicking the Find button.

Background

When I first programmed on the Windows platform, Microsoft used the term “Document-View-Controller”. Nowadays, it is called Model View Controller. Martin Fowler's articles such as “GUI Architecture” - http://martinfowler.com/eaaDev/uiArchs.html and “Separated Presentation” - http://martinfowler.com/eaaDev/SeparatedPresentation.html explain MVC and other related architectures like MVP (Model View Presenter) and its descendants in detail. John Gossman coined the term MVVM in his blog, http://blogs.msdn.com/b/johngossman/archive/2005/10/08/478683.aspx, that I highly recommend reading. Even though MVVM is meant for WPF/Silverlight apps with data binding, we should be able to use the idea in Windows Forms applications. And that’s what Aviad Ezra wrote in his blog - http://aviadezra.blogspot.com/2009/08/mvp-mvvm-winforms-data-binding.html where he defines MVP-VM as “The Windows Forms (WinForms) equivalent of WPF MVVM. The MVP-VM (Model View Presenter – Model View) pattern is a tailor made solution for WinForms applications that require full testing coverage and extensively use data binding for syncing the presentation with the domain model.”

I focused more on how I could make the form class file as thin as possible, rather than seeking the full test coverage capability by using the MVP-VM pattern. In fact, the form.cs file of the example application has just 87 lines of C# code, with all the validation and error processing built-in, which are required for any product quality code. The code is FxCop clean and StyleCop clean except for the documentation rules. I also used Code Contracts - http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx, which is new to .NET 4, instead of using more traditional Debug.Assert() or If-argument-is-invalid-then-throw-ArgumentException style error checking. Why, you ask. It's all about code readability. I'll explain it as we go through the code and encounter them. Still, it's not a part of Visual Studio 2010. So, I created two downloads with and without the Code Contracts Rewriter so that those of you who don't have it with your VS2010 can still compile and run the software.

Software Requirements Specification

First of all, let's make sure the requirements of this simple application. Regardless of the size of the program, writing the SRS (Software Requirements Specification) or User Story, depending on the adopted SDLC (Software Development Life Cycle) model in your project, should be a programmer's second nature.

  1. The software shall solve the Project Euler problem no. 1 in the general form.
  2. The user shall be able to provide arbitrary numbers of positive 32 bit integers for the "seeds".
  3. The user shall be able to specify the upper limit within the range of a 32 bit integer.
  4. The software shall remember the last setups and the answer when the user starts the application.
  5. The software shall show the progress of solving the given problem.
  6. The user shall be able to cancel the solving process while it is in progress.
  7. The software shall use Windows Forms technology as a design constraint.

Solution - The Model (Business Domain)

I designed a single domain class that solves the given problem and named it Solver. It has a Max property for the search limit, a bucket of integers that you can put arbitrary numbers of Seeds in, and a method called FindSumOfMultiplesOfSeedsBelowMax(), which is the meat of this application. In addition to those three public members, I added a property called Answer to harvest the result. You can easily imagine how you can use this class - .Max = 1000, .Seeds.Add(3), .Seeds.Add(5), call the method, and get the result by .Answer. The resultant class spec is as follows:

Looks like a nice, simple API, doesn't it? Both Max and Answer are integer properties, and the bucket is named Seeds. I named the bucket class type PositiveIntegerSetCollection because it should accept only positive integers. The bucket should be a mathematical “set”, rather than a simple collection of integers in that it should ignore duplicated elements. Anyways, I created this spec and was able to throw it to another member of the project, well, in theory. Unfortunately, because I'm the only member of the project, I accepted the role. An important thing here is that you better concentrate on the business logic and not be distracted by secondary requirements such as showing the progress or retrieving/saving the setups and the answer. Those are the UI stuff. They should have nothing to do with your way of solving the problem.

If this was a classroom project in courses like C# Fundamentals, you'd be fine just submitting the implementation of this class. However, as real world product quality software, it's just a beginning. You'll have to add the UI and satisfy the SRS with all the validation and error handlings without losing the responsiveness of the UI. As it turns out, the Solver class occupies just a little more than 10% of the entire code if you count the number of lines, except for the Designer.cs file. 90% of the code needed to be consumed elsewhere.

Solution - UI (a.k.a. View)

For the UI, I decided to use two textboxes to accept Max and Seeds, a label to show Answer, a button to start the calculation, and another button to cancel it in the middle of the calculation, as shown at the beginning of this article. Some users may want to have a different UI; a textbox for a single seed, an “Add” button that adds the seed to a list box, and a “Remove” button that removes the selected seed on the list box, for example. This tells us that the UI can be very fragile, which is precisely why we shouldn't consider the UI issue (too much) when we design the business domain classes. Otherwise, changes in the UI would easily propagate to the domain classes and you would have to modify and retest them.

To show the progress, I placed a ProgressBar right next to the Cancel button. I chose to show it only while the software is solving the problem because when it is not solving the problem, it is just another distraction from the user's point of view. I also chose to disable the button and the textboxes while it is solving the problem to lock out the user. On the other hand, the Cancel button needs to be enabled only while the software is solving the problem. Thus, the software needs to control the Visible property of the progress bar and the Enabled property of the buttons and the textboxes. To show the user input errors on the textboxes, I decided to use the ErrorProvider, rather than reporting errors on a modal dialog box.

As I said, the form class has only 87 lines of code that follows:

using System;
using System.ComponentModel;
using System.Windows.Forms;

namespace DataBinding
{
    public partial class SolverForm : Form
    {
        private readonly SolverPresenter presenter;

        public SolverForm()
        {
            InitializeComponent();
        }

        public SolverForm(SolverPresenter presenter) : this()
        {
            this.presenter = presenter;
            this.presenter.FindAsyncCompleted += Presenter_FindAsyncCompleted;
            solverViewModelBindingSource.DataSource = this.presenter.SolverViewModel;

            // Setting the error provider's DataSource needs to be done *AFTER*
            // setting the binding source's DataSource. Otherwise, binding to the
            // Visible property wouldn't work (always invisible).
            // See details on http://social.msdn.microsoft.com/Forums/en-US/
            //      winformsdatacontrols/thread/269e0803-4e05-462e-91b7-abd768615f68/
            //      #f0ac03e5-eb18-4abf-a36c-619aeea13382
            errorProvider1.DataSource = solverViewModelBindingSource;
        }

        private static void Presenter_FindAsyncCompleted(object sender, 
                            AsyncCompletedEventArgs e)
        {
            try
            {
                if (e.Error != null)
                {
                    throw e.Error;
                }
            }
            catch (OperationFailedException ex)
            {
                MessageBox.Show(
                    ex.Message,
                    Application.ProductName,
                    MessageBoxButtons.OK,
                    MessageBoxIcon.None,
                    MessageBoxDefaultButton.Button1,
                    MessageBoxOptions.DefaultDesktopOnly);
            }
        }

        private void FindButton_Click(object sender, EventArgs e)
        {
            try
            {
                this.presenter.FindClicked();
            }
            catch (OperationFailedException ex)
            {
                MessageBox.Show(
                    ex.Message,
                    Application.ProductName,
                    MessageBoxButtons.OK,
                    MessageBoxIcon.None,
                    MessageBoxDefaultButton.Button1,
                    MessageBoxOptions.DefaultDesktopOnly);
            }
        }

        private void CancelButton_Click(object sender, EventArgs e)
        {
            try
            {
                this.presenter.CancelClicked();
            }
            catch (OperationFailedException ex)
            {
                MessageBox.Show(
                    ex.Message,
                    Application.ProductName,
                    MessageBoxButtons.OK,
                    MessageBoxIcon.None,
                    MessageBoxDefaultButton.Button1,
                    MessageBoxOptions.DefaultDesktopOnly);
            }
        }
    }
}

Notice that the class has only five methods - two constructors, two button click event handlers, and another event handler to show error messages that may have been captured while the software was trying to solve the given problem. The first constructor is the default one and is nothing special. The second one has four lines that bind the ViewModel, BindingSource, and ErrorProvider altogether and set the presenter's completed event handler. The Main() in the Program.cs calls this constructor. Of course, the form.Designer.cs file contains the statements to actually bind the visual controls’ properties to the corresponding ViewModel’s properties in terms of the DataSource. To add a DataSource to your project, first build the project and then select the <Data/Add New Data Source…> menu on VS2010, select “Object” in the first page of the wizard, and choose SolverViewModel. The Windows Forms Designer will automatically create a solverViewModelBindingSource and put it on the component tray. If you don’t know how to bind the visual control’s properties to the BindingSource on the IDE, refer to http://msdn.microsoft.com/en-us/library/sw223a62.aspx.

The button click event handlers just delegate the user commands to the presenter. The Form class doesn't need to know anything about the business. Of course, every event handler in the Form class needs to catch a specific exception. Otherwise, it would easily crash. I created a custom OperationFailedException for this purpose. We don't need to dig down what exceptions the presenter throws; every function in every class is designed to throw an OperationFailedException if it “does not do what its name suggests” [CLARK]. The only thing the UI's event handlers need to do is to catch it and display the error message on a message box. All error processing other than showing the message must be done in the appropriate places under the UI surface.

There’s a golden rule in FxCop; Do not swallow errors by catching nonspecific exceptions, such as System.Exception, System.SystemException, and so on [CWALINA/ABRAMS]. So, always catch only the specific exceptions that you know how to handle.

Solution - The ViewModel

Once you have finished designing the business domain and the UI, now is the time to think about the ViewModel part of the pattern. The ViewModel object binds to the visuals via a BindingSource. Since we use textboxes and a label, the Max, Seeds, and Answer properties must be of type string, rather than integer or the bucket of integer. Even though you could bind an integer property directly to a textbox's Text property, I don't recommend doing it because then parsing the string to integer is done somewhere in the binding pipeline and you have no control on the error message that needs to be shown to the user when the string can't be properly converted to an integer. If you do, the framework spits out the cryptic "Input String was not in a Correct Format", which the end user may not understand at all. Unfortunately, there's no way to override this message within the current .NET Framework. So, the lesson here is to create a ViewModel class that has string properties that bind to the UI's text properties and handle parsing/formatting in your code so that you have full control on how a string should be converted to a numeric value and vice versa.

By the way, I personally have never used data binding before I decided to use the MVP-VM pattern in my next project. After all, “Haven’t we been told since Visual Basic 2.0 that we should never use binding? It incorporated patterns that were not extensible, it did not use good programming practices, and it frequently didn’t work as expected.” [KURATA]. However, Microsoft seems to have finally fixed the problems (well, most of them) when they released .NET 2.0 back in 2005. I also highly recommend reading the book, Doing Objects in Visual Basic 2005. In fact, I decided to incorporate the Validation class described in the book almost as is.

In addition to the three properties that correspond to those of the Model class, we need to have the ProgressPercentage, ProgressBarVisible, ControlsEnabled, and ControlsDisabled properties to show the progress and to control the visuals. Of these seven properties, Max and Seeds are the ones that we need to implement validation on their Set property calls that are done when the user leaves the controls on the form. If there're any validation errors, we want to show them with the help of the ErrorProvider. This is done via the IDataErrorInfo interface. For the rest of the properties, we need to "Notify" the changes to the binding pipeline so that the visual controls properly reflect the changes. This is done via the INotifyPropertyChanged interface.

Accommodating the properties that bind to the corresponding UI properties is the primary responsibility of the ViewModel. Validating the user input is the second most important responsibility. It is also responsible for propagating the validated user inputs (the Max string and the Seeds string) directly to the Model's (i.e., Solver class) corresponding business properties by adapting the interface. This third responsibility is also important because business models’ API (Max integer and Seeds bucket of integer) almost always differ from the UI (Max string and Seeds string). Somebody has to adapt the interface. Also, ViewModel's role is passive in that it doesn't actually execute the commands the user issues. The active role is given to the Presenter.

Solution - Presenter

Now is the time to explain the last actor of the MVP-VM pattern: the Presenter. In WPF, there's support for the ICommand interface in such a way that the ViewModel can expose commands to the View directly. So, there's no need for the Presenter. In Windows Forms, however, there's no such support in data binding, so we have to do it ourselves. That's the Presenter's role - to actually implement the commands the View receives from the user. In this application, it's the FindClicked() and the CancelClicked() functions that are called from the View when the user clicks the Find or Cancel button. Given below is the code for these functions:

public void FindClicked()
{
    this.SolverViewModel.Answer = string.Empty;
    this.SolverViewModel.Validate();
    if (this.SolverViewModel.IsValid)
    {
        this.SolverViewModel.UpdateModel();
        this.SolverViewModel.ProgressBarVisible = true;
        this.SolverViewModel.ControlsEnabled = false;
        this.solver.FindSumOfMultiplesOfSeedsBelowMaxAsync();
    }
}
public void CancelClicked()
{
    this.solver.CancelAsync();
}

private void Solver_AsyncCompleted(object sender, AsyncCompletedEventArgs e)
{
    this.OnFindAsyncCompleted(e);
    this.SolverViewModel.ProgressPercentage = 0;
    this.SolverViewModel.ProgressBarVisible = false;
    this.SolverViewModel.ControlsEnabled = true;
}

FindClicked() plays the key role in this application in that it orchestrates the Model (AsynchronousSolver) and the ViewModel (SolverViewModel) to get the job done. It first clears the Answer label and lets the ViewModel validate the user inputs - Max string and Seeds string. If they are valid, it lets the ViewModel update the Model's corresponding input properties, show the progress bar, disable the relevant controls, and finally calls the Model's business function – FindSumOfMultiplesOfSeedsBelowMaxAsync(). CancelClicked() tries to cancel the execution of FindSumOfMultiplesOfSeedsBelowMaxAsync() by calling its CancelAsync(). When the Model finishes working - regardless of either successfully having solved the problem and updated its Answer property, or having thrown an exception due to some error - it always raises an AsyncCompleted event at the end. The Presenter subscribes to the event to first let the UI show any error messages by raising the FindAsyncCompleted event, and then clears the progress percentage, makes the progress bar invisible, and finally enables the controls again for the next set of input values.

Some of you may wonder where the IView interface is. Usually, the Presenter communicates with the View via the IView interface, rather than communicating directly with the form class in order to reduce coupling. Well, I didn’t need it. There’s no need for the Presenter to access the View in this particular application. YAGNI applies here.

Class Diagram

The above is the class diagram of this application. The gray ones are .NET classes, the blue one is the UI, the beige ones are this application's specific business classes, and the white ones are general plumbing classes that you can use in other projects. As you can see from the diagram, SolverViewModel is the most complex in that it associates with five other classes. As a result, I put it in the center of the diagram. Notice that SolverViewModel has no direct association with the form class.

Adding the navigability arrows on the association lines in the class diagram can be very helpful to identify dependencies - i.e., you'd need mock objects for whatever the classes the outgoing arrows point to when you unit-test the class. For example, take a look at SolverPresenter. To unit-test its methods, you will need to create two fake objects that "mock" AsynchronousSolver and SolverViewModel. On the other hand, if a class has no outgoing association lines, it is quite independent (no coupling), and is very easy to create unit tests because you don’t need any mock objects. Progress and Solver are examples of such a class.

I needed a minor modification to the Solver class in order to report the progress. You need to expose some of your internal members to report the progress of the internal process. One way of doing it is to use Inheritance. Thus, FirstArgIsMultipleOfSecondArg() was changed from a private method to protected, and also virtualized to add the reporting function. This literally exposes your “guts” – not a good thing in terms of encapsulation – but showing the progress of its internal process precisely means showing its internals. So, I couldn’t help but do it. The derived class (AsynchronousSolver) overrides the method of the base class so that it behaves a little differently, as shown below:

protected override bool FirstArgIsMultipleOfSecondArg(int firstArg, int secondArg)
{
    if (this.progress.IsTimeToReportProgress(1))
    {
        this.backgroundWorker.ReportProgress(this.progress.CurrentPercent);
    }

    if (this.backgroundWorker.CancellationPending)
    {
        throw new OperationCanceledException();
    }        return base.FirstArgIsMultipleOfSecondArg(firstArg, secondArg);
}

First, check to see if it's time to report progress. If so, it calls the BackgroundWorker's ReportProgress(). Then, it checks if the user hits the Cancel button by asking the BackgroundWorker. If so, it throws an OperationCanceledException. Finally, it calls the base class' overridden method. As you can see, I used a BackgroundWorker to keep the UI responsive and also to allow the user to cancel the operation in the middle of the process. When Max is 1000, the calculations would finish immediately. But, you might be pleased to be able to cancel the calculations if you add five more zeros for Max.

Now, you may wonder where the BackgroundWorker is created. AsynchronousSolver not only overrides FirstArgIsMultipleOfSecondArg(), but also adds a new method called FindSumbOfMultiplesOfSeedsBelowMaxAsync() that is an asynchronous version of FindSumbOfMultiplesOfSeedsBelowMax(), like so:

public void FindSumOfMultiplesOfSeedsBelowMaxAsync()
{
    if (this.backgroundWorker != null)
    {
        this.backgroundWorker.Dispose();
    }

    this.backgroundWorker = new BackgroundWorker { WorkerReportsProgress = true, 
                                                   WorkerSupportsCancellation = true };
    this.backgroundWorker.ProgressChanged += this.BackgroundWorker_ProgressChanged;
    this.backgroundWorker.RunWorkerCompleted += this.BackgroundWorker_RunWorkerCompleted;
    this.backgroundWorker.DoWork += this.BackgroundWorker_DoWork;
    this.backgroundWorker.RunWorkerAsync();
}

You can see, it creates a new BackgroundWorker, hooks the events up, and runs the task in a Thread Pool thread. The Presenter calls this method instead of the base class' synchronous version directly. The event handlers are coded as follows:

private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    // Any exceptions thrown while running the code below and we don't catch
    // here, will be automatically transferred
    // to RunWorkerCompletedEventArgs.Error property.
    // Because we catch the OperationCanceledException that we are using to
    // indicate that the user canceled the operation,
    // RunWorkerCompletedEventArgs.Error will be null.

    // When an exception is thrown and you are running it from within Visual Studio,
    // VS will pop up an "xxxException was unhandled by user code" message box.
    // Do not be warned, since this is by design. Simply clicking the Run button
    // again will let VS continue running the code. Read the following excerpt.
                // Excerpt from MSDN BackgroundWorker.DoWork Event:
    //   "If the operation raises an exception that your code does not handle,
    //   the BackgroundWorker catches the exception and passes it into the
                //   RunWorkerCompleted event handler, where it is exposed as the Error
    //   property of System.ComponentModel.RunWorkerCompletedEventArgs.
    //   If you are running under the Visual Studio debugger, the debugger
    //   will break at the point in the DoWork event handler where the unhandled
    //   exception was raised."
    try
    {
        this.progress = new Progress(Seeds.Count, Max);
        FindSumOfMultiplesOfSeedsBelowMax();
    }
    catch (OperationCanceledException)
    {
        // e.Cancel will be transferred to RunWorkerCompletedEventArgs.Canceled.
                        e.Cancel = true;
    }
}

private void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    this.OnProgressChanged(new ProgressChangedEventArgs(e.ProgressPercentage, null));
}

private void BackgroundWorker_RunWorkerCompleted(object sender, 
                              RunWorkerCompletedEventArgs e)
{
    this.OnPropertyChanged(new PropertyChangedEventArgs(
                           SolverViewModel.AnswerPropertyName));
    this.OnAsyncCompleted(new AsyncCompletedEventArgs(e.Error, e.Cancelled, null));
}

The DoWork event is raised in the worker thread. It creates a Progress object and calls the base class' most important function - FindSumbOfMultiplesOfSeedsBelowMax(). Notice that these are enclosed with a Try/Catch block, and if an OperationCanceledException is thrown, it sets the DoWorkEventArgs.Cancel property to true, which will be transferred to the RunWorkerCompletedEventArgs.Canceled property when the RunWorkerCompleted event is raised.

Also, read the comment on this method very carefully. When you run the code within VS2010 and FindSumbOfMultiplesOfSeedsBelowMax() throws an OperationFailedException, VS2010 will catch it and pop up the "xxxException was unhandled by user code" dialog box. This is so because we don't catch it, and before the exception reaches the BackgroundWorker, VS2010 has to catch it.

The ProgressChanged event is raised when BackgroundWorker.ReportProgress() is called in FirstArgIsMultipleOfSecondArg(). The event handler raises its own ProgressChanged event that is different from the one raised by BackgroundWorker. SolverViewModel subscribes to this event to update the progress bar. BackgroundWorker's ProgressChanged event is raised in the UI thread, so there's no need to marshal the call back to the UI thread.

Finally, the RunWorkerCompleted event is raised when FindSumbOfMultiplesOfSeedsBelowMax() finishes execution either by successfully finishing the entire calculation, or is prematurely aborted by the user, or some exceptions were thrown during the calculations. Regardless of the reason, the event handler raises a PropertyChanged event to update the Answer. Again, SolverViewModel subscribes to the event. Then, it raises an AsyncCompleted event to which the SolverPresenter subscribes.

Miscellaneous Programming Tips

Persistence<T> – One of the requirements of this application says that we have to save and retrieve the setups (Max and Seeds) and the result (Answer) when the user closes and opens the application. Persistence<T> is responsible for doing it by exposing two APIs – ReadObject() and WriteObject(T). .NET 3.0 added the DataContractSerializer class for serving general persistence scenarios, and I used it in this application. It’s very easy to use; just add the <DataContract> attribute to the class that you want to persist, and <DataMember> to the members you want to actually save/retrieve. The resultant XML file looks like so:

<?xml version="1.0" encoding="utf-8"?>
<Solver xmlns:i="http://www.w3.org/2001/XMLSchema-instance" 
         xmlns="http://schemas.datacontract.org/2004/07/DataBinding">
  <Answer i:nil="true" />
  <Max>1000</Max>
  <Seeds xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
    <d2p1:int>3</d2p1:int>
    <d2p1:int>5</d2p1:int>
  </Seeds>
</Solver>

Persistence<T> takes care of everything including catching the relevant exceptions and re-throwing an OperationFailedException with hopefully user-friendly error messages. Again, do not catch general exceptions here.

PositiveIntegerSetCollection - This class inherits from HashSet<int> that was newly added to .NET 4.0. We need a mathematical set instead of a general collection because we don’t want duplicated elements, which is exactly what the set is for. The only thing it does differently from the base class is to throw an ArgumentOutOfRangeException if the client of this class tries to add a negative integer, as shown below:

public new bool Add(int value)
{
    Contract.Requires<ArgumentOutOfRangeException>(value > 0, 
                        "Must be a positive integer.");
    return base.Add(value);
}

It needs the new keyword because the base class’ Add() is not virtualized and can’t be overridden. The first statement is the Code Contracts where I check the pre-condition. I like the syntax very much because of its readability. The contract requires value>0; otherwise, I will throw an ArgumentOutOfRangeException with such and such error message. And of course, do not catch this exception in your code because if it really happens, it’s a bug (not an exception), and the only way to “handle” the bug is to debug the problem, fix it, and redistribute the software. Catching it in your code won’t buy you anything. Let your application die miserably in front of the user; he or she would let you know that you didn’t test your app thoroughly, which was your responsibility and should have never happened in the first place.

Unfortunately, Code Contracts is not yet fully a part of VS2010 as of writing this article (June 2010), even if .NET 4.0 has already supported it. In other words, you can build this app error free even if you have never downloaded Code Contracts, but you will have a runtime error when you run it because throwing exceptions require the Code Contracts Rewriter. This is why I put two downloads, with and without throwing exceptions.

Conclusion

I showed how MVP-VM pattern can help you create the thinnest possible form.cs file in product quality Windows Forms applications.

References

  • [CLARK] Jason Clark, p. 218, “Framework Design Guidelines” Second Edition, Krzysztof Cwalina, Brad Abrams, 2008
  • [CWALINA/ABRAMS] p.227, “Framework Design Guidelines” Second Edition, Krzysztof Cwalina, Brad Abrams, 2008
  • [KURATA] “Doing Objects in Visual Basic 2005”, Deborah Kurata, 2007

History

  • 6/17/2010: Initial post.

License

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

About the Author

tetsushmz
Software Developer (Senior)
Japan Japan
Member
He started his career as a PDP-11 assembly language programmer in downtown Tokyo, learning what "patience" in real life means by punching a 110 baud ASR-33 Teletype frantically. He used to be able to put in the absolute loader sequence through the switch panel without consulting the DEC programming card.
 
Since then, his computer language experiences include 8051 assembly, FOCAL, BASIC, FORTRAN-IV, Turbo/MS C, and VB.
 
Now, he lives with his wife, two college kids (I'm broke), and two cats in Westerville, Ohio.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5memberPrasad Khandekar26 Apr '13 - 0:04 
Very well written article, Thank's
QuestionCongrats!!!memberFábioPandolfo8 Feb '12 - 1:01 
Very nice article, thanks a lot!
GeneralMy vote of 5memberGuy Baseke14 Feb '11 - 4:37 
Clear writting and goes straight to point.
GeneralAwesome, just one question. ;)memberfujiwara13 Oct '10 - 0:35 
Heya, really awesome article. I've been looking for something like this for Windows Forms.
 
Anyway, I'm writing a parser based on this, and I was hoping that you may be able to point me in the right direction.
 
The problem is that each ViewModel need to contain information for 45-50 TextBoxes. Which would turn out to be a massive amount of strings to contain all the information.
 
What technique would you use to contain this many strings? simple arrays or maybe datasets? This is not done progressively so there is just one Property Update needed. Wink | ;)
 
Best Regards, Erik
GeneralVery helpful article! Some design questions...memberJens Wingerter28 Jul '10 - 10:15 
First thanks a lot for this article. I studied it and want to refactoring my application with this pattern. There are some things which are not clear to me.
 
1) In my application is one MainForm which is equivalent to your SolverForm. Additionally I need some more forms to input data which I open from MainForm. What do you recommend to handle the validation and the passing of this input forms to the SolverViewModel? IDataErrorInfo is very nice and I would like to use it as well in the input forms. And do I use DataBinding in this input forms and how to bind them to SolverViewModel.
2) My SolverViewModel is getting very big because on my MainForm there are a lot of controlls how can I useful struct them. Shall I use several SolverViewModels?
3) How would you handle MessageBoxes. When my Solver is sticking in a very long process and encountered a problem and it is necessary to ask the user how to proceed. Abort or Continue? In this case I want to show a MessageBox. How shall I call it from the solver?
 
Thanks
GeneralRe: Very helpful article! Some design questions...membertetsushmz29 Jul '10 - 0:56 
Those are all good questions. I may add some more requirements to address your issues such as (a) The user shall be able to enter a comment text on a separate form, (b) The comment shall also be persisted, (c) If calculation takes more than 5 seconds, the app shall pop up a message box to ask whether to Abort or Continue.
 
Can be a nice exercise for Sunday afternoon...
GeneralRe: Very helpful article! Some design questions...membermultitasker1 Aug '10 - 3:26 
I would very much appreciate it.
 
Can you tell me as well why is it necessary to call backgroundWorker.Dispose() explicitly.
        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    if (this.backgroundWorker != null)
                    {
                        this.backgroundWorker.Dispose();
                    }
                }
            }
 
            this.disposed = true;
        }

GeneralRe: Very helpful article! Some design questions...membertetsushmz1 Aug '10 - 8:39 
That is because the AsynchronousSolver contains an instance of BackgroundWorker, which is a disposable type. I followed a suggestion in the [CWALINA/ABRAMS] book:
 
"DO implement the Basic Dispose Pattern on types containing instances of disposable types. See section 9.4.1 for details on the basic pattern. If a type is responsible for the lifetime of other disposable objects, developers need a way to dispose of them, too."
 
My other call to the BackgroundWorker.Dispose() followed the final part of the excerpt.
GeneralRe: Very helpful article! Some design questions...membertetsushmz16 Aug '10 - 8:57 
I have finally had time to do this. The first section deals with the new requirement (c) - If calculation takes more than 5 seconds, the app shall pop up a message box to ask whether to Abort or Continue.
To do that, I further inherited the AsynchronousSolver to make the MoreComplexSolver as given:
 
using System;
using System.Diagnostics;
using System.Runtime.Serialization;
 
namespace DataBinding
{
    [DataContract(Name = "Solver")]
    public class MoreComplexSolver : AsynchronousSolver
    {
        private Stopwatch stopwatch;
        private bool askedUser;
 
        public event EventHandler<UserResponseRequiredEventArgs> UserResponseRequired;
 
        [DataMember]
        public string Comments { get; set; }
 
        public static MoreComplexSolver CreateDefaultSolver()
        {
            var solver = new MoreComplexSolver { Max = 1000 };
            solver.Seeds.Add(3);
            solver.Seeds.Add(5);
            return solver;
        }
 
        protected override void DoWork()
        {
            this.askedUser = false;
            this.stopwatch = new Stopwatch();
            this.stopwatch.Start();
            base.DoWork();
        }
 
        protected override bool FirstArgIsMultipleOfSecondArg(int firstArg, int secondArg)
        {
            if (this.NeedsToAskUserIfHeWantsToContinue())
            {
                var e = new UserResponseRequiredEventArgs
                    {
                        Message = "Calculation may take longer. Do you want to continue?"
                    };
                this.OnUserResponseRequired(e);
                if (!e.UserWantsToContinue)
                {
                    throw new OperationCanceledException();
                }
            }
 
            return base.FirstArgIsMultipleOfSecondArg(firstArg, secondArg);
        }
 
        protected virtual void OnUserResponseRequired(UserResponseRequiredEventArgs e)
        {
            var handler = this.UserResponseRequired;
            if (handler != null)
            {
                handler(this, e);
            }
        }
 
        private bool NeedsToAskUserIfHeWantsToContinue()
        {
            var result = this.stopwatch.ElapsedMilliseconds > 5000 && !this.askedUser;
            if (result)
            {
                this.askedUser = true;
            }
 
            return result;
        }
    }
}
It has a stop watch and overrides the base class’s DoWork() in order to start the stop watch before starting the calculations. To detect the 5 seconds elapsed time, it overrides the FirstArgIsMultipleOfSecondArg(). If 5 seconds has already elapsed, it raises a UserResponseRequired event that I created to interrogate the user. This event blocks until the user dismiss the message box by either clicking the Yes or No button in response to the “Do you want to continue?” message.
 
I extracted the statements in the try block within the BackgroundWorker_DoWork() in AsynchronousSolver class and made it a virtual function named DoWork() so that the extended classes can add its own functionality as shown below:
        protected virtual void DoWork()
        {
            this.progress = new Progress(Seeds.Count, Max);
            FindSumOfMultiplesOfSeedsBelowMax();
        }
 
        private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
        {            try
            {
                this.DoWork();
            }
            catch (OperationCanceledException)
            {
                // e.Cancel will be transferred to RunWorkerCompletedEventArgs.Canceled.
                e.Cancel = true;
            }
        }
 
The UserResponseRequiredEventArgs has a Boolean property named “UserWantsToContinue” that reflects the user’s choice. The function checks the value and throws the OperationCanceledException if the user chose to do so.
 
The UserResponseRequiredEventArgs class is easy:
using System;
 
namespace DataBinding
{
    public class UserResponseRequiredEventArgs : EventArgs
    {
        public string Message { get; set; }
 
        public bool UserWantsToContinue { get; set; }
    }
}
I also added the Comments property and the DataContract attribute to the MoreComplexSolver class to persist it.
 
Now, who subscribes to the UserResponseRequired event? The SolverPresenter does like so:
        public Control Control { get; set; }
 
        public SolverPresenter(MoreComplexSolver solver)
        {
            this.solver = solver;
            this.solver.AsyncCompleted += this.Solver_AsyncCompleted;
            this.solver.UserResponseRequired += this.Solver_UserResponseRequired;
            this.SolverViewModel = new SolverViewModel(solver);
            this.SolverViewModel.InitializeBoundProperties();
        }
 
        private void Solver_UserResponseRequired(object sender, UserResponseRequiredEventArgs e)
        {
            if (this.Control.InvokeRequired)
            {
                this.Control.Invoke(new EventHandler<UserResponseRequiredEventArgs>(this.Solver_UserResponseRequired), new object[] { this, e });
            }
            else
            {
                var result = MessageBox.Show(
                    e.Message,
                    Application.ProductName,
                    MessageBoxButtons.YesNo,
                    MessageBoxIcon.None,
                    MessageBoxDefaultButton.Button1,
                    MessageBoxOptions.DefaultDesktopOnly);
                e.UserWantsToContinue = result == DialogResult.Yes;
            }
        }
The class got a new property called Control to marshal the event raised in a worker thread to the UI thread. The constructor hooks the event. The event handler uses the InvokeRequired/Invoke pattern to call it recursively in the UI thread. At the second time it is called, it just pops up the message box and transfers the user’s choice to the UserWantsToContinue Boolean.
 
To actually use the InvokeRequired/Invoke pattern, the SolverForm class had to supply the Control property of the SolverPresenter in its constructor:
            this.presenter.Control = this;
The above changes to my article satisfy the requirement (c).
 
Now, let’s talk about how I dealt with the requirement (a) - The user shall be able to enter a comment text on a separate form.
I created a form that has a multi-line textbox, “Update Comments” button, and “Cancel” button. The window title is “Edit Comments”. The class exactly looks like the SolverForm with the MVP-VM pattern as shown:
using System;
using System.Windows.Forms;
 
namespace DataBinding
{
    public partial class EditCommentsForm : Form
    {
        private readonly CommentsPresenter presenter;
 
        public EditCommentsForm()
        {
            InitializeComponent();
        }
 
        public EditCommentsForm(CommentsPresenter presenter)
            : this()
        {
            this.presenter = presenter;
            commentsViewModelBindingSource.DataSource = this.presenter.CommentsViewModel;
        }
 
        private void UpdateCommentsButton_Click(object sender, EventArgs e)
        {
            try
            {
                this.presenter.UpdateCommentsClicked();
            }
            catch (OperationFailedException ex)
            {
                MessageBox.Show(
                    ex.Message,
                    Application.ProductName,
                    MessageBoxButtons.OK,
                    MessageBoxIcon.None,
                    MessageBoxDefaultButton.Button1,
                    MessageBoxOptions.DefaultDesktopOnly);
            }
        }
 
        private void CancelButton_Click(object sender, EventArgs e)
        {
            try
            {
                this.presenter.CancelClicked();
            }
            catch (OperationFailedException ex)
            {
                MessageBox.Show(
                    ex.Message,
                    Application.ProductName,
                    MessageBoxButtons.OK,
                    MessageBoxIcon.None,
                    MessageBoxDefaultButton.Button1,
                    MessageBoxOptions.DefaultDesktopOnly);
            }
        }
    }
}
As you can see, the form class is very thin because of the MVP-VM pattern. I created another presenter named CommentsPresenter that drives the form. The form class connects the DataSource property of the BindingSource to the presenter’s ViewModel. As usual, the form class just delegates the buttons’ click events to the presenter.
 
The CommentsPresenter looks like this:
using System;
 
namespace DataBinding
{
    public class CommentsPresenter : IDisposable
    {
        private EditCommentsForm editCommentsForm;
        private bool disposed;
 
        public CommentsPresenter(string comments)
        {
            this.Comments = comments;
            this.CommentsViewModel = new CommentsViewModel { Comments = comments };
        }
 
        public CommentsViewModel CommentsViewModel { get; private set; }
 
        public string Comments { get; private set; }
 
        public void ShowForm()
        {
            this.editCommentsForm = new EditCommentsForm(this);
            this.editCommentsForm.ShowDialog();
        }
 
        public void UpdateCommentsClicked()
        {
            this.Comments = this.GetUpdatedComments();
            this.editCommentsForm.Close();
        }
 
        public void CancelClicked()
        {
            this.editCommentsForm.Close();
        }
 
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }
 
        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    if (this.editCommentsForm != null)
                    {
                        this.editCommentsForm.Dispose();
                    }
                }
            }
 
            this.disposed = true;
        }
 
        private string GetUpdatedComments()
        {
            return this.CommentsViewModel.Comments;
        }
    }
}
It has its own Comments property to support the “Cancel” – i.e., on the UpdateCommentsClicked() it retrieves the Comments property from the ViewModel. Just like the SolverPresenter creates the SolverViewModel, the CommentsPresenter creates the CommentsViewModel whose sole member is the Comments property:
namespace DataBinding
{
    public class CommentsViewModel
    {
        public string Comments { get; set; }
    }
}
 
The SolverForm now has a button named “Edit Comments” and a read-only textbox to show the comments to the user. It delegates the button click event to the SolverPresenter like so:
        public void EditCommentsClicked()
        {
            var presenter = new CommentsPresenter(this.solver.Comments);
            presenter.ShowForm();
            this.SolverViewModel.UpdateComments(presenter.Comments);
        }
The SolverPresenter creates a CommentsPresenter, shows the EditComments form, and updates the SolverViewModel’s Comments property by retrieving the Comments property from the Commentspresenter.
 
The SolverViewModel added the Comments property and some support functions:
        public const string CommentsPropertyName           = "Comments";
        private string comments;
        public string Comments
        {
            get
            {
                return this.comments;
            }
 
            set
            {
                if (this.comments != value)
                {
                    this.comments = value;
                    this.OnPropertyChanged(new PropertyChangedEventArgs(CommentsPropertyName));
                }
            }
        }
 
        public void InitializeBoundProperties()
        {
            this.Max = this.solver.Max.ToString(CultureInfo.CurrentCulture);
            this.Seeds = this.GetSeeds();
            this.Answer = this.GetAnswerFromSolver();
            this.Comments = this.solver.Comments;
        }
 
        public void UpdateComments(string newComments)
        {
            this.Comments = newComments;
            this.solver.Comments = newComments;
        }
The above changes satisfy the requirement (a).
 
I could send you complete code if you let me know your email address.
GeneralRe: Very helpful article! Some design questions... [modified]membermultitasker29 Aug '10 - 7:31 
Thank you for your effort! It looks very good.
One thing. Is it allowed regarding the MVP-VM pattern to call a MessageBox in the presenter? Personally I do not see any problems by doing this.

modified on Sunday, August 29, 2010 4:38 PM

GeneralThanks again for the articlememberseeblunt28 Jun '10 - 0:31 
I posted my code into the replies on previous thread. I must admit that removing logic from the Form code , using Solver and ViewModel/Validator requires some getting used to. I have refactored some existing form code that was previously 'spaghetti code' in forms with good result. I have problems with some of my more 'monolithic' forms that have dozens of UI Controls and complicated logic. These are difficult to transform in to Presenter-VM style.
Some initial problems were finding the correct way to databind List Controls.
 
I found the CheckListBox, for example, to be impossible to databind directly to the VM for properties such as CheckedItems & CheckedIndices. These require the presenter to transmit the changes to VM and/or Solver. What I ended up is creating a Function in the form to retrieve the CheckedValues and passing a delegate via Presenter to the Solver that calls the function.
 
Basically I found a lot of issues regarding where to place 'properties' and how to distribute 'responsibilities' between the different layers. It becomes a design issue, often more complex than the initial problem it was trying to solve.
 
However, after persisting at it I am finding that the design issues are becoming easier to solve with repitition. The benefits of separating 'logic' from 'state' and from 'UI' plus simplification of threading are continuing to intrigue me. Transitioning code to other platforms eg WPF should be a whole lot easier in future. I will be looking at converting 'spahetti form code' vs 'Present-VM' code , into WPF soon, as an exercise.
 
So thanks again for offering your perplexing P-VM solution for discussion.
GeneralRe: Thanks again for the articlemembertetsushmz28 Jun '10 - 3:32 
I’m glad to hear that you were able to refactor some of your existing form code by using this pattern.
 
As to the CheckedListBox, I don’t know if its DataSource property works or not. MSDN says “This property is not relevant for this class”. If it doesn’t, one way to do that is to extend the class like this article - Bindable CheckedListBox. Microsoft might have intentionally hidden the DataSource and DisplayMember from the CheckedListBox, though. So, I don’t know if it would work as the article suggests. There might be a hidden cost.
 
Yes. How to distribute responsibilities between the classes is the intrinsic raison detre for refactoring. Bob Martin says “a class or module should have one, and only one, reason to change” in his book called “Clean Code – A Handbook of Agile Software Craftsmanship”. As a matter of fact, one of my colleagues doesn’t like the idea of VM having three responsibilities – accommodating the view properties, validating the user inputs, and adapting the API to and from the Model.
 
My take on this is to focus more on thinning the form file as I wrote in the article. I hate those huge, ugly, and monolithic form files. MVP-VM can mitigate this.
GeneralGreat articlemembervic_bang25 Jun '10 - 3:00 
Even I can understand it Smile | :)
 
One small typo, though: the CancelClicked() function is declared as public void icked() in the code.
GeneralRe: Great articlemembertetsushmz27 Jun '10 - 11:22 
Thanks. I'll ask the Code Project Editor to correct the typo.
GeneralThank youmemberJonn Lim21 Jun '10 - 3:01 
I'm currently developing my first Winform App at work and this is exactly what I was looking for. Well done good sir.
GeneralRe: Thank youmembertetsushmz21 Jun '10 - 15:26 
Thanks.
GeneralSeparation of concern between layersmemberseeblunt20 Jun '10 - 13:11 
Separation of concern between layers can be enhanced to allow less crossover dependency of layers.
The ViewModel does not require knowledge of the Solver.
The Presenter layer can be charged with the role of loading values to / from the viewmodel/solver.
Only the Presenter layer needs to have a dependency on both viewmodel & solver.
FYI and Cheers
GeneralRe: Separation of concern between layersmembertetsushmz20 Jun '10 - 15:25 
So you are refactoring my code. Why don't you put your ViewModel class?
GeneralRe: Separation of concern between layersmemberseeblunt20 Jun '10 - 15:48 
OK. Here is the View Model
Note it derives from ThreadedViewModelBase (which has properties such as IsRunning, ProgressPercentage,ProgressBarVisible, IsReady,HasRun,NeverRun,CanCancel,Cancelled,HasAnswer) to simplify re-use.
 
 internal class ExampleSolverViewModel : ThreadedViewModelBase
    {
        public const string MaxPropertyName                = "Max";                 // These string constants must match the property name. Otherwise, data binding wouldn't work.
        public const string SeedsPropertyName              = "Seeds";
        private string max;
        private string seeds;
        private string answer;
        public ExampleSolverViewModel()
            : base(new ExampleSolverValidator())
        {
        }
        /// <summary>
        /// Input by UI
        /// </summary>
        public string Max
        {
            get
            {
                return this.max;
            }
            set
            {
                if (this.max != value)
                {
                    this.max = value;
                    this.ValidateMax();
                }
            }
        }
        /// <summary>
        /// Input by UI
        /// </summary>
        public string Seeds
        {
            get
            {
                return this.seeds;
            }
            set
            {
                if (this.seeds != value)
                {
                    this.seeds = value;
                    this.ValidateSeeds();
                }                
            }
        }
        /// <summary>
        /// OutPut to UI
        /// </summary>
        public string Answer
        {
            get
            {
                return this.answer;
            }
            set
            {
                if (this.answer != value)
                {
                    this.answer = value;
                    this.OnPropertyChanged(new PropertyChangedEventArgs("Answer"));
                }
            }
        }
        public void Validate()
        {
            this.ValidateMax();
            this.ValidateSeeds();
        }
        internal int ValidateMax()
        {
            return Validation.ValidatePositiveInteger(MaxPropertyName, this.max);
        }
        internal Collection<int> ValidateSeeds()
        {
            return ((ExampleSolverValidator)Validation).ValidateCsvForPositiveIntegers(SeedsPropertyName, this.seeds, 10);
        }
    }

GeneralRe: Separation of concern between layersmembertetsushmz21 Jun '10 - 15:25 
I see what you are doing. Will try it myself to see if decoupling the model from the viewmodel would make more sense.
GeneralRe: Separation of concern between layersmemberseeblunt21 Jun '10 - 21:55 
Here is the base classes for Actual View Model
Has some limited logic for state handling
internal class ThreadedViewModelBase : ViewModelBase
    {
        private bool _canCancel;
        private bool _cancellled;
        private bool _hasRun;
        private bool _isReady = true;
        private bool _isRunning;
        private bool _neverRun = true;
        private bool _progressBarVisible;
        private int _progressPercentage;
 
        protected ThreadedViewModelBase(Validator validation) : base(validation)
        {
        }
 
        public int ProgressPercentage
        {
            get { return _progressPercentage; }
 
            set
            {
                if (_progressPercentage != value)
                {
                    _progressPercentage = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("ProgressPercentage"));
                }
            }
        }
 
        public bool ProgressBarVisible
        {
            get { return _progressBarVisible; }
 
            set
            {
                if (_progressBarVisible != value)
                {
                    _progressBarVisible = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("ProgressBarVisible"));
                }
            }
        }
 
        public bool IsReady
        {
            get { return _isReady; }
 
            set
            {
                if (_isReady != value)
                {
                    _isReady = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("IsReady"));
                    IsRunning = !value;
                    CanCancel = !value;
              
                }
            }
        }
 
        public bool IsRunning
        {
            get { return _isRunning; }
 
            private set
            {
                if (_isRunning != value)
                {
                    _isRunning = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("IsRunning"));
                    if (!_hasRun)
                    {
                        HasRun = true;
                    }
                }
            }
        }
 

        public bool HasRun
        {
            get { return _hasRun; }
 
            private set
            {
                if (_hasRun != value)
                {
                    _hasRun = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("HasRun"));
                    NeverRun = !_hasRun;
                    HasAnswer = _hasRun && !_cancellled;
                }
            }
        }
 
        public bool NeverRun
        {
            get { return _neverRun; }
 
            private set
            {
                if (_neverRun != value)
                {
                    _neverRun = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("NeverRun"));
                }
            }
        }
 
        public bool CanCancel
        {
            get { return _isRunning && _canCancel; }
 
            set
            {
                if (_isRunning && _canCancel != value)
                {
                    _canCancel = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("CanCancel"));
                }
            }
        }
 
        public bool Cancelled
        {
            get { return _cancellled; }
 
            set
            {
                if (_cancellled != value)
                {
                    _cancellled = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("Cancelled"));
                    HasAnswer = _hasRun && !_cancellled;
                }
            }
        }
 
        private bool _hasAnswer;
 
        public bool HasAnswer
        {
            get { return _hasAnswer; }
            set
            {
                if (this._hasAnswer != value)
                {
                    this._hasAnswer = value;
                    this.OnPropertyChanged(new PropertyChangedEventArgs("HasAnswer"));
                }
            }
        }
 
And the ViewModel Base
internal class ViewModelBase : IDataErrorInfo, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected ViewModelBase(Validator validation)
        {
            Validation = validation;
        }
        [Browsable(false)]
        public bool IsValid
        {
            get { return this.Validation.ErrorCount == 0; }
        }
 
        [Browsable(false)]
        public string Error
        {
            get { return this.Validation.ToString(); }
        }
 
        protected Validator Validation { get; private set; }
 
        public Exception RuntimeError { get; set; }
 
        public string RuntimeErrorMessage { 
            get
        {
            return RuntimeError == null ? string.Empty : RuntimeError.Message;
        }
        }
        /// <summary>
        /// Gets the error message for the property with the given name.
        /// </summary>
        /// <returns>
        /// The error message for the property. The default is a null.
        /// </returns>
        /// <param name="columnName">The name of the property whose error message to get. 
        ///                 </param>
        public string this[string columnName]
        {
            get
            {
                return this.Validation[columnName];
            }
        }
 
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            var handler = this.PropertyChanged;
            if (handler != null)
            {
                handler(this, e);
            }            
        }
    }

GeneralRe: Separation of concern between layersmemberseeblunt21 Jun '10 - 21:58 
The Validators
 
ExampleSolverValidator
public class ExampleSolverValidator : Validator
    {
 
        public Collection<int> ValidateCsvForPositiveIntegers(string propertyName, string value, int maxCount)
        {
            if (string.IsNullOrEmpty(propertyName))
            {
                throw new ArgumentNullOrEmptyException(propertyName);
            }
            Collection<int> result = new Collection<int>();
     
                Clear(propertyName);
                foreach (string v in value.Split(new[] { ',' }, maxCount))
                {
                    int output;
                    if (!int.TryParse(v, out output))
                    {
                        string s = "Cannot convert to one or more valid whole numbers \"" + value + "\".";
                        AddValidationError(propertyName, s);
                        return null;
                    }
 
                    if (output <= 0)
                    {
                        AddValidationError(propertyName, "Must all be positive whole numbers.");
                        return null;
                    }
 
                    result.Add(output);
                }
         
 
            return result;
        }
    }
 
And Validator (base class)
public class Validator
    {
        private readonly Dictionary<string, string> _errorList = new Dictionary<string, string>();
 

        public int ValidatePositiveInteger(string propertyName, string value)
        {
            //Contract.Requires(!string.IsNullOrEmpty(propertyName));
            //Contract.Requires(value != null);

            Clear(propertyName);
            int result;
            if (!int.TryParse(value, out result))
            {
                AddValidationError(propertyName, propertyName + " Must be a valid whole number.");
                return 0;
            }
 
            if (result <= 0)
            {
                AddValidationError(propertyName, propertyName+ " Must be a positive whole number.");
                return 0;
            }
 
            return result;
        }
        public int ValidatePositiveInteger(string propertyName, int value)
        {
 
            Clear(propertyName);
  
 
            if (value <= 0)
            {
                AddValidationError(propertyName, propertyName+ " Must be a positive whole number.");
            }
 
            return value;
        }
 
        public int ErrorCount
        {
            get { return this._errorList.Count; }
        }
 
        public string this[string propertyName]
        {
            get
            {
                if (this._errorList.ContainsKey(propertyName))
                {
                    return this._errorList[propertyName];
                }
 
                return null;
            }
        }
 
        public override string ToString()
        {
            var sb = new StringBuilder();
            foreach (var s in this._errorList.Values)
            {
                sb.AppendLine(s);
            }
 
            return sb.ToString();
        }
 
        public void Clear(string propertyName)
        {
            if (this._errorList.ContainsKey(propertyName))
            {
                this._errorList.Remove(propertyName);
            }
        }
 
        protected void AddValidationError(string propertyName, string message)
        {
            if (this._errorList.ContainsKey(propertyName))
            {
                var existingMessage = this._errorList[propertyName];
                if (!existingMessage.Contains(message))
                {
                    this._errorList[propertyName] += "; " + message;
                }
            }
            else
            {
                this._errorList.Add(propertyName, message);
            }
        }
    }

GeneralRe: Separation of concern between layersmemberseeblunt21 Jun '10 - 22:01 
SOLVERS
 
internal class ExampleSolver : ISolver<ProgressChangedEventArgs, int>
    {
        //[DataMember(Name = "Seeds")]
        private readonly PositiveIntegerSetCollection seeds = new PositiveIntegerSetCollection();
        private int _iteration;
        private IReportProgress<ProgressChangedEventArgs> _reporter;
        private int _result;
        private int max;
        private HashSet<int> multiples;
        private int _maxIteration;
        private DoWorkEventArgs _doWorkEventArgs;
 

        public static AsynchronousSolver<ExampleSolver,string, ProgressChangedEventArgs, int> CreateDefaultSolver()
        {
            ExampleSolver solver = new ExampleSolver {Max = 1000};
            AsynchronousSolver<ExampleSolver, string, ProgressChangedEventArgs, int> asyncsolver = new AsynchronousSolver<ExampleSolver,string, ProgressChangedEventArgs, int>(solver);
            solver.Seeds.Add(3);
            solver.Seeds.Add(5);
            return asyncsolver;
        }
 
        //[DataMember]
        public int Max
        {
            get { return max; }
 
            set
            {
                //Contract.Requires(value > 0, "Max must be a positive integer.");
                max = value;
            }
        }
 
        public int MaxIteration
        {
            get { return seeds == null ? 0 : max*seeds.Count; }
        }
 
        public PositiveIntegerSetCollection Seeds
        {
            get { return seeds; }
        }
 
        #region ISolver<ProgressChangedEventArgs,int> Members
 
        public void Run(IReportProgress<ProgressChangedEventArgs> reporter,DoWorkEventArgs e)
        {
            _doWorkEventArgs = e;
            _iteration = 0;
            _maxIteration = this.Max * this.Seeds.Count;
 
            //Contract.Requires(this.Max > 0, "Max must be assigned a positive integer before calling this function.");
            //Contract.Requires(this.Seeds.Count > 0, "At least one seed needs to be added before calling this function.");
            _reporter = reporter;
            _result = 0;
            multiples = new HashSet<int>();
            GetMultiples();
            GetSum();
            e.Result = Result;
        }
 
        public int Result
        {
            get { return _result; }
        }
 
        #endregion
 
        private void AccumulateResults(int value)
        {
            try
            {
                multiples.Add(value);
            }
            catch (OutOfMemoryException ex)
            {
                multiples.Clear();
                string msg = string.Format
                    (CultureInfo.CurrentCulture, "Out of memory. Too many multiples ({0}) found...\nPlease try different set of values.", multiples.Count);
                throw new OperationFailedException(msg, ex);
            }
        }
 

        private bool FirstArgIsMultipleOfSecondArg(int firstArg, int secondArg)
        {
 
            if (_reporter != null && _maxIteration>0)
                _reporter.ReportProgress(_iteration*100/_maxIteration);
            Thread.Sleep(25); 
                _iteration++;
            return firstArg%secondArg == 0;
        }
 
        private void GetMultiples()
        {
            foreach (int v in seeds)
            {
 
                GetMultiplesOf(v);
            }
        }
 
        private void GetMultiplesOf(int seed)
        {
            for (int i = 1; i < Max; i++)
            {
                if (_reporter.CancellationPending)
                {
                    _doWorkEventArgs.Cancel = true;
                    _doWorkEventArgs.Result = 0;
                    return;
                }
                 
                if (FirstArgIsMultipleOfSecondArg(i, seed))
                {
                    AccumulateResults(i);
                }
            }
        }
 
        private void GetSum()
        {
            try
            {
                _result = multiples.Sum();
            }
            catch (OverflowException ex)
            {
                string msg = string.Format
                    (
                        CultureInfo.CurrentCulture,
                        "The sum was greater than {0}.\nThis program can not handle numbers larger than that. Please try different set of values.", int.MaxValue);
                throw new OperationFailedException(msg, ex);
            }
            finally
            {
                multiples.Clear();
            }
        }
    }
 
Interface
  internal interface ISolver<TProgress,  TResult>
    {
        void Run(IReportProgress<TProgress> arg,DoWorkEventArgs e);
        TResult Result { get; }
    }
 
AsynchronousSolver
 
internal class AsynchronousSolver<TSolver , TArgument, TProgress, TResult>
        where TSolver : ISolver<TProgress, TResult>
    {
 
        private readonly TSolver _solver;
 
        private BackgroundWorkerAdvanced<TArgument, TProgress, TResult> _backgroundWorker;
 

        public event EventHandler<BackgroundWorkerAdvanced<TArgument, TProgress, TResult>.ProgressChangedAdvancedEventArgs> ProgressChanged;
 
        public event PropertyChangedEventHandler PropertyChanged;
 
        public event EventHandler<BackgroundWorkerAdvanced<TArgument, TProgress, TResult>.RunWorkerCompletedAdvancedEventArgs> AsyncCompleted;
 
        internal AsynchronousSolver(TSolver solver)
        {
            _solver = solver;
  
        }
 

   
 
        #region ISolver<TArgument,TResult> Members
 

        public TResult Result
        {
            get { return Solver.Result; }
        }
 
        public TSolver Solver
        {
            get { return _solver; }
        }
 
        #endregion
 

 
        public void Run(TArgument arg)
        {
 

            this._backgroundWorker = new BackgroundWorkerAdvanced<TArgument, TProgress, TResult> { WorkerReportsProgress = true, WorkerSupportsCancellation = true };
            this._backgroundWorker.ProgressChanged += BackgroundWorkerProgressChanged;
            this._backgroundWorker.RunWorkerCompleted += BackgroundWorkerRunWorkerCompleted;
            this._backgroundWorker.DoWork += BackgroundWorkerDoWork;
            this._backgroundWorker.RunWorkerAsync(arg);
        }
 
        void BackgroundWorkerDoWork(object sender, BackgroundWorkerAdvanced<TArgument, TProgress, TResult>.DoWorkEventArgs e)
        {
            // Any exceptions thrown while running the code below and we don't catch
            // here, will be automatically transferred to RunWorkerCompletedEventArgs.Error property.
            // Because we catch the OperationCanceledException that we are using to
            // indicate that the user canceled the operation, RunWorkerCompletedEventArgs.Error
            // will be null.

            // When an exception is thrown and you are running it from within Visual Studio,
            // VS will pop up an "xxxException was unhandled by user code" message box.
            // Do not be warned, since this is by design. Simply clicking the Run button
            // again will let VS continue running the code. Read the following excerpt.
            // Excerpt from MSDN BackgroundWorker.DoWork Event:
            //   "If the operation raises an exception that your code does not handle,
            //   the BackgroundWorker catches the exception and passes it into the
            //   RunWorkerCompleted event handler, where it is exposed as the Error
            //   property of System.ComponentModel.RunWorkerCompletedEventArgs.
            //   If you are running under the Visual Studio debugger, the debugger
            //   will break at the point in the DoWork event handler where the unhandled
            //   exception was raised."
            try
            {
                Solver.Run(_backgroundWorker, e);
            }
            catch (OperationCanceledException)
            {
                // e.Cancel will be transferred to RunWorkerCompletedEventArgs.Canceled.
                e.Cancel = true;
            }
        }
 
        void BackgroundWorkerRunWorkerCompleted(object sender, BackgroundWorkerAdvanced<TArgument, TProgress, TResult>.RunWorkerCompletedAdvancedEventArgs e)
        {
          this.OnAsyncCompleted(e);
        }
 
        void BackgroundWorkerProgressChanged(object sender, BackgroundWorkerAdvanced<TArgument, TProgress, TResult>.ProgressChangedAdvancedEventArgs e)
        {
            this.OnProgressChanged(e);
        }
 
        public void CancelAsync()
        {
            this._backgroundWorker.CancelAsync();
        }
 

 
  
 
        private void OnProgressChanged(BackgroundWorkerAdvanced<TArgument, TProgress, TResult>.ProgressChangedAdvancedEventArgs e)
        {
            var handler = this.ProgressChanged;
            if (handler != null)
            {
                handler(this, e);
            }
        }
 
        private void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null)
            {
                handler(this, e);
            }
        }
 
        private void OnAsyncCompleted(BackgroundWorkerAdvanced<TArgument, TProgress, TResult>.RunWorkerCompletedAdvancedEventArgs e)
        {
            var handler = this.AsyncCompleted;
            if (handler != null)
            {
            
                handler(this, e);
            }
        }
 

 

    }

GeneralRe: Separation of concern between layers [modified]memberseeblunt21 Jun '10 - 22:03 
BackgroundWorkerAdvanced
This is a genericised BackgroundWorker
/// <summary>
    /// Executes an operation on a separate thread.
    /// </summary>
    /// <typeparam name="TArgument">The type of argument passed to the worker.</typeparam>
    /// <typeparam name="TProgress">The type of ProgressChangedEventArgs.UserState.</typeparam>
    /// <typeparam name="TResult">The type of result retrieved from the worker.</typeparam>

    public class BackgroundWorkerAdvanced<TArgument, TProgress, TResult> : IReportProgress<TProgress>
    {
        #region Constants
        public sealed class RunWorkerCompletedAdvancedEventArgs : RunWorkerCompletedEventArgs
        {
            #region Constructors
 
            /// <summary>
            /// Initializes a new instance of the CurrentPatient.RunWorkerCompletedEventArgs class.
            /// </summary>
            /// <param name="result">The result of an asynchronous operation.</param>
            /// <param name="error">Any error that occurred during the asynchronous operation.</param>
            /// <param name="cancelled">A value indicating whether the asynchronous operation was cancelled.</param>
            public RunWorkerCompletedAdvancedEventArgs(TResult result, Exception error, bool cancelled):base(result,error,cancelled )
            {
 

            }
 
            #endregion
 
     
 
            #region Properties
 
            /// <summary>
            /// Gets a value that represents the result of an asynchronous operation.
            /// </summary>
            public TResult MyResult
            {
                get
                {
                    return base.Result!=null?(TResult) base.Result:default(TResult);
                }
        
            }
 
            #endregion
        }
        /// <summary>
        /// Provides data for the CurrentPatient.BackgroundWorker.DoWork event.
        /// </summary>
        /// <typeparam name="TArgument">The type of argument passed to the worker.</typeparam>
        /// <typeparam name="TResult">The type of result retrieved from the worker.</typeparam>
        public sealed class DoWorkEventArgs : System.ComponentModel.DoWorkEventArgs
        {
            #region Constructors
 
            /// <summary>
            /// Initializes a new instance of the CurrentPatient.DoWorkEventArgs class.
            /// </summary>
            /// <param name="argument">Specifies an argument for an asynchronous operation.</param>
            public DoWorkEventArgs(TArgument argument)
                : base(argument)
            {
 
            }
 
            #endregion
 
            #region Properties
 
            /// <summary>
            /// Gets a value that represents the argument of an asynchronous operation.
            /// </summary>
            public TArgument MyArgument
            {
                get { return (TArgument)base.Argument; }
 
            }
            /// <summary>
            /// Gets a value that represents the result of an asynchronous operation.
            /// </summary>
            public TResult MyResult
            {
                get
                {
                    return base.Result != null ? (TResult)base.Result : default(TResult);
                }
 
            }
 
            #endregion
        }
 
        /// <summary>
        /// Provides data for the CurrentPatient.BackgroundWorker.ProgressChanged event.
        /// </summary>
        /// <typeparam name="T">The type of UserState.</typeparam>
        public sealed class ProgressChangedAdvancedEventArgs : EventArgs
        {
            #region Constructors
 
            /// <summary>
            /// Initializes a new instance of the CurrentPatient.ProgressChangedEventArgs class.
            /// </summary>
            /// <param name="progressPercentage">The percentage of an asynchronous task that has been completed.</param>
            /// <param name="userState">A unique user state.</param>
            public ProgressChangedAdvancedEventArgs(int progressPercentage, TProgress userState)
            {
                ProgressPercentage = progressPercentage;
                UserState = userState;
            }
 
            #endregion
 
            #region Properties
 
            /// <summary>
            /// Gets the asynchronous task progress percentage.
            /// </summary>
            public int ProgressPercentage { get; private set; }
 
            /// <summary>
            /// Gets a unique user state.
            /// </summary>
            public TProgress UserState { get; private set; }
 
            #endregion
        }
 
        public const int MinProgress = 0;
        public const int MaxProgress = 100;
 
        #endregion
 
        #region Events
 
        /// <summary>
        /// Occurs when CurrentPatient.BackgroundWorker.RunWorkerAsync() is called.
        /// </summary>
        public event EventHandler<DoWorkEventArgs> DoWork;
        /// <summary>
        /// Occurs when CurrentPatient.BackgroundWorker.ReportProgress(System.Int32) is called.
        /// </summary>
        public event EventHandler<ProgressChangedAdvancedEventArgs> ProgressChanged;
        /// <summary>
        /// Occurs when the background operation has completed, has been canceled, or has raised an exception.
        /// </summary>
        public event EventHandler<RunWorkerCompletedAdvancedEventArgs> RunWorkerCompleted;
 
        #endregion
 
        #region Fields
 
        private AsyncOperation asyncOperation = null;
        private readonly ThreadStart threadStart;
        private readonly SendOrPostCallback operationCompleted;
        private readonly SendOrPostCallback progressReporter;
 
        #endregion
 
        #region Constructor
 
        /// <summary>
        /// Initializes a new instance of the CurrentPatient.BackgroundWorker class.
        /// </summary>
        public BackgroundWorkerAdvanced()
        {
            threadStart = new ThreadStart(WorkerThreadStart);
            operationCompleted = new SendOrPostCallback(AsyncOperationCompleted);
            progressReporter = new SendOrPostCallback(ProgressReporter);
            WorkerReportsProgress = true;
            WorkerSupportsCancellation = true;
        }
 
        #endregion
 
        #region Properties
 
        /// <summary>
        /// Gets a value indicating whether the application has requested cancellation of a background operation.
        /// </summary>
        public bool CancellationPending
        {
            get;
            private set;
        }
        /// <summary>
        /// Gets a value indicating whether the CurrentPatient.BackgroundWorker
        /// is running an asynchronous operation.
        /// </summary>
        public bool IsBusy
        {
            get;
            private set;
        }
        /// <summary>
        /// Gets or sets a value indicating whether the CurrentPatient.BackgroundWorker
        /// can report progress updates. The default is true.
        /// </summary>
        public bool WorkerReportsProgress
        {
            get;
            set;
        }
        /// <summary>
        /// Gets or sets a value indicating whether the CurrentPatient.BackgroundWorker
        /// supports asynchronous cancellation. The default is true.
        /// </summary>
        public bool WorkerSupportsCancellation
        {
            get;
            set;
        }
 
        #endregion
 
        #region Methods
 
        private void AsyncOperationCompleted(object state)
        {
            IsBusy = false;
            CancellationPending = false;
            RunWorkerCompletedAdvancedEventArgs args = (RunWorkerCompletedAdvancedEventArgs) state;
 

            OnRunWorkerCompleted(args);
        }
        /// <summary>
        /// Requests cancellation of a pending background operation.
        /// </summary>
        /// <returns>true, if the CurrentPatient.BackgroundWorker supports cancellation;
        /// otherwise, false.</returns>
        public bool CancelAsync()
        {
            if (!WorkerSupportsCancellation)
                return false;
            CancellationPending = true;
            return true;
        }
        /// <summary>
        /// Raises the CurrentPatient.BackgroundWorker.DoWork event.
        /// </summary>
        /// <param name="e">A CurrentPatient.DoWorkEventArgs
        /// that contains the event data.</param>
        protected virtual void OnDoWork(DoWorkEventArgs e)
        {
            EventHandler<DoWorkEventArgs> eh = DoWork;
            if (eh != null)
                eh(this, e);
        }
        /// <summary>
        /// Raises the CurrentPatient.BackgroundWorker.ProgressChanged event.
        /// </summary>
        /// <param name="e">A CurrentPatient.ProgressChangedEventArgs
        /// that contains the event data.</param>
        protected virtual void OnProgressChanged(ProgressChangedAdvancedEventArgs e)
        {
            EventHandler<ProgressChangedAdvancedEventArgs> eh = ProgressChanged;
            if (eh != null)
                eh(this, e);
        }
        /// <summary>
        /// Raises the CurrentPatient.BackgroundWorker.RunWorkerCompleted event.
        /// </summary>
        /// <param name="e">A CurrentPatient.RunWorkerCompletedEventArgs
        /// that contains the event data.</param>
        protected virtual void OnRunWorkerCompleted(RunWorkerCompletedAdvancedEventArgs e)
        {
            EventHandler<RunWorkerCompletedAdvancedEventArgs> eh = RunWorkerCompleted;
            if (eh != null)
                eh(this, e);
        }
        private void ProgressReporter(object state)
        {
            OnProgressChanged((ProgressChangedAdvancedEventArgs)state);
        }
        /// <summary>
        /// Raises the CurrentPatient.BackgroundWorker.ProgressChanged event.
        /// </summary>
        /// <param name="percentProgress">The percentage, from 0 to 100, of the background operation that is complete.</param>
        /// <returns>true, if the CurrentPatient.BackgroundWorker reports progress;
        /// otherwise, false.</returns>
        public bool ReportProgress(int percentProgress)
        {
            return ReportProgress(percentProgress, default(TProgress));
        }
        /// <summary>
        /// Raises the CurrentPatient.BackgroundWorker.ProgressChanged event.
        /// </summary>
        /// <param name="percentProgress">The percentage, from MinProgress to MaxProgress,
        /// of the background operation that is complete.</param>
        /// <param name="userState">An object to be passed to the
        /// CurrentPatient.BackgroundWorker.ProgressChanged event.</param>
        /// <returns>true, if the CurrentPatient.BackgroundWorker reports progress;
        /// otherwise, false.</returns>
        public bool ReportProgress(int percentProgress, TProgress userState)
        {
            if (!WorkerReportsProgress)
                return false;
            if (percentProgress < MinProgress)
                percentProgress = MinProgress;
            else if (percentProgress > MaxProgress)
                percentProgress = MaxProgress;
            ProgressChangedAdvancedEventArgs args = new ProgressChangedAdvancedEventArgs(percentProgress, userState);
            if (asyncOperation != null)
                asyncOperation.Post(progressReporter, args);
            else
                progressReporter(args);
            return true;
        }
        /// <summary>
        /// Starts execution of a background operation.
        /// </summary>
        /// <returns>true, if the CurrentPatient.BackgroundWorker isn't busy;
        /// otherwise, false.</returns>
        public bool RunWorkerAsync()
        {
            return RunWorkerAsync(default(TArgument));
        }
        /// <summary>
        /// Starts execution of a background operation.
        /// </summary>
        /// <param name="argument">A parameter for use by the background operation to be executed in the
        /// CurrentPatient.BackgroundWorker.DoWork event handler.</param>
        /// <returns>true, if the CurrentPatient.BackgroundWorker isn't busy;
        /// otherwise, false.</returns>
        public bool RunWorkerAsync(TArgument argument)
        {
            if (IsBusy)
                return false;
            IsBusy = true;
            CancellationPending = false;
            asyncOperation = AsyncOperationManager.CreateOperation(argument);
            threadStart.BeginInvoke(null, null);
            return true;
        }
        private void WorkerThreadStart()
        {
            TResult workerResult = default(TResult);
            Exception error = null;
            bool cancelled = false;
            try
            {
                DoWorkEventArgs doWorkArgs =
                    new DoWorkEventArgs((TArgument)asyncOperation.UserSuppliedState);
                OnDoWork(doWorkArgs);
                if (doWorkArgs.Cancel)
                    cancelled = true;
                else
                    workerResult = doWorkArgs.MyResult;
            }
            catch (Exception exception)
            {
                error = exception;
            }
            RunWorkerCompletedAdvancedEventArgs e = new RunWorkerCompletedAdvancedEventArgs(workerResult, error, cancelled);
            asyncOperation.PostOperationCompleted(operationCompleted, e);
        }
 
        #endregion
    }
IReport Progress
 public interface IReportProgress<TProgress>
    {
        /// <summary>
        /// Raises the CurrentPatient.BackgroundWorker.ProgressChanged event.
        /// </summary>
        /// <param name="percentProgress">The percentage, from 0 to 100, of the background operation that is complete.</param>
        /// <returns>true, if the CurrentPatient.BackgroundWorker reports progress;
        /// otherwise, false.</returns>
        bool ReportProgress(int percentProgress);
 
        /// <summary>
        /// Raises the CurrentPatient.BackgroundWorker.ProgressChanged event.
        /// </summary>
        /// <param name="percentProgress">The percentage, from MinProgress to MaxProgress,
        /// of the background operation that is complete.</param>
        /// <param name="userState">An object to be passed to the
        /// CurrentPatient.BackgroundWorker.ProgressChanged event.</param>
        /// <returns>true, if the CurrentPatient.BackgroundWorker reports progress;
        /// otherwise, false.</returns>
        bool ReportProgress(int percentProgress, TProgress userState);
 
        /// <summary>
        /// Gets a value indicating whether the application has requested cancellation of a background operation.
        /// </summary>
        bool CancellationPending
        {
            get;
      
        }
    }

modified on Tuesday, June 22, 2010 4:24 AM

GeneralRe: Separation of concern between layers [modified]memberdevnet2478 Nov '11 - 19:39 
Hi seeblunt,
Trying to implement your modified suggestion but I think you have no presenter.
Do you have a download somewhere?thanks
thanks a lot

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

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130523.1 | Last Updated 17 Jun 2010
Article Copyright 2010 by tetsushmz
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid