JAL Computing

C++COMProgramming .NET Mac Palm CPP/CLI Hobbies

 

Home
Up

Chapter 31 "C# Windows Form model-Controller-view with Events"

In this chapter I am going to graft what I am going to call the m_C_v (model-Controller-view) pattern onto a Windows Form program. I am also going to use this opportunity to introduce Events from our twisted viewpoint of object oriented programming. Although this is not an example of the classic MVC architecture of SmallTalk, it does borrow from SmallTalk the "separation of concerns" into a Model, a View and a Controller. The Model class encapsulates the application logic and algorithms (including the data store). The Controller responds to user and system events. The View draws the presentation. 

This is a thread safe implementation of our Mortgage Calculator that uses Controller Events to notify one or more View components of changes in Model state. One _could_ view the use of events as a method of "sending messages to interested objects" that implement the event delegate. Information is passed between  "layers" using thread safe immutable structures embedded in these "messages". This could also be done using immutable classes.

In this more complex application design pattern, the Controller acts as an intermediary between the Model object and the View objects. The Controller adds a level of indirection that can be used to bind more than one View object to a Model state, adds an opportunity to add intermediary logic between the Model and View, and decouples the interaction between the Model and View. In more complex programs, this decoupling could be used to minimize event related memory leaks (see lapsed listener). The Controller can be used to fire events and register/unregister listeners to Events sent to dynamically created Views using an interface such as the IViewableEvents interface introduced in Chapter 32.

Note: An alternative approach to the lapsed listener conundrum is to use weak references and a proxy object. In the WeakEvent pattern, the WeakEventManager acts as a proxy between the supplier and the client. The proxy holds weak references to the client, allowing the client to be garbage collected when the client goes out of scope. In the WeakEvent pattern, the client must implement the IWeakEventListener and implement the ReceiveWeakEvent method.

For instance, here is a sample ReceiveWeakEvent From MSDN:

bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
    if (managerType == typeof(ClockwiseSpinEventManager))
    {
        OnClockwiseSpin(sender, (SpinEventArgs)e);
    }
    else if (managerType == typeof(CounterclockwiseSpinEventManager))
    {
        OnCounterclockwiseSpin(sender, (SpinEventArgs)e);
    }
    else
    {
        return false;       // unrecognized event
    }
    return true;
}
private void OnClockwiseSpin(object sender, SpinEventArgs e) {
    //do something here...
}
private void OnCounterclockwiseSpin(object sender, SpinEventArgs e) {
    //do something here...
}

Finally, the Controller can be stateful. One of the major advantages of the m_C_v architecture is that it supports the display of multiple Views of the same data. For instance, a Mortgage Calculation can be displayed as raw data in a series of text boxes, in a graphical pie chart comparing total interest and principal, or in table displaying the amortization schedule.

Some have argued that the Controller acts as the "glue" between the Model and View.

Fig. 31.1 mCv Pattern with Controller Events Indirectly Updating View

As noted previously, this is not an example of the classic MVC of Smalltalk. In Smalltalk, the View is indirectly updated by the Model, an example of the Observable pattern. In more complex programs the Observable pattern can lead to unexpected memory leaks when Views "subscribe to events and then subsequently fall out of scope".

Fig. 31.2 SmallTalk MCV Pattern with Model Indirectly Updating View (Observable Pattern)

In this project, the Controller is central to the solution and talks to the Model and Views. This is similar to the approach that I have used in Cocoa under OSX where the Controller can own the Model and View. In this project the Controller talks directly to the Model and indirectly to the Views using Events. Any object can request an update to the Model by talking directly to the Controller.

In an extreme case, classes can be added as Adapters that add logic between the Controller and a specific View component. For instance, in an extreme example, each TextBox object could have its own Adapter object that contains the logic to populate a specific TextBox based on the Model state. In this more extreme approach, each Adapter could be implemented as an inner class. To demonstrate the use of inner classes as Adapters, we add an inner class MortgageTotal to the project that calculates the total cost of mortgage payments over the lifetime of the loan. An obvious example of object oriented overkill, but hopefully a simple enough class to demonstrate the concept of using an Adapter. 

Fig. 31.3 mCv Pattern with an Adapter

The end product of this separation of Model, View and Controller is that we can write and test Model classes completely independent of the View classes and vice-versa. The Model classes can be unit tested as a console applications. Drag and drop visual components can be developed independently by different developers. Custom Adapters and Controllers can then be written to tie together the independently developed and tested visual components and application models. The Controller can be implemented to both register and unregister View Events, potentially avoiding a "lapsed listener" memory leak. Finally, it is also possible to have more than one Controller. Two controllers can be used to enforce user permissions by registering the appropriate Views with each controller.

Note: The reader may want to briefly review the much simpler M-VC design pattern in Chapter 3.

Delegates as Type Safe Pointers to Polymorphic Methods

This is really our first project that creates a custom event and represents a practical demonstration of events. As twisted object oriented programmers we look at delegates as type safe pointers to polymorphic methods. In this context, we can think of delegates as the close cousins of interfaces; both can represent types and both can be used to implement polymorphism, to have more than one form. 

For instance, there are two general ways we could implement a callback between the Controller and interested Views in this program. The first method uses an interface INotify that contains a method Notify that looks like this:

        public interface INotify
        {
            bool Notify(Mortgage.Calculation mc);
        }

Any View component that wanted to be notified of changes in the Model state would implement the Mortgage.INotify interface. The View could then register an interest in notifications by registering itself with the Controller and passing a reference to itself of the type Mortgage.INotify. When the Model state changed, the Controller could then iterate over the array of INotify objects and invoke Notify(mc) on each object in the array.

The .Net approach is to use Events. This represents both a challenge and an opportunity. The challenge is to understand and implement Events, which IMHO, is not self evident. The opportunity is that this represents a chance to actually understand how Events can be used in a practical manner. This can be another "AHA" moment, when the power of Events finally becomes apparent.

Lets review Events and Delegates. A Delegate is a type safer pointer to "some" method. It is an improvement over function pointers in C++.

According to MSDN:   "A delegate is a type that safely encapsulates a method, similar to a function pointer in C and C++. Unlike C function pointers, delegates are object-oriented, type safe, and secure."  

Perhaps the "AHA" moment is at hand. An Interface can also "encapsulate" (hide the implementation details) a method! The actual implementation details of the Interface method is unknown to the caller. The actual implementation of "some" Delegate method is unknown to the caller!  Thus both Delegates and Interfaces provide a definition of the public view of a method, but not the implementation details. Thus both Delegates and Interfaces support polymorphic behavior. Different implementations of "some" method can have more than one form.

According to MSDN: "Both delegates and interfaces allow a class designer to separate type declarations and implementation. A given interface can be inherited and implemented by any class or struct; a delegate can created for a method on any class, as long as the method fits the method signature for the delegate. An interface reference or a delegate can be used by an object with no knowledge of the class that implements the interface or delegate method.""

C programmers may have a better insight into delegates as function pointers to polymorphic methods. The C standard library qsort algorithm uses function pointers to implement a qsort algorithm. The qsort algorithm does not provide any implementation of the compare function, only a function prototype. The user of the qsort must provide a "compare" function that matches the function prototype. In C# this can be accomplished using the IComparer interface. Again, we see the similarity between using interfaces and using function pointers (delegates) to invoke methods that can have more than one form (polymorphism).

One advantage of Delegates over Interfaces is that they provide a built in mechanism for registering listeners and invoking calls on all interested listeners. If we used interfaces to notify View objects, we would have to roll our own arrayOfListeners and write our own registration and notification methods. If we use Events and Delegates, the registration and invoke plumbing is automatic.

The first step in using Events is to define the Delegate, a type safe pointer to some method as in:

public delegate void NotifyHandler(object source, MortgageEventArgs args);

Note the standardized nomenclature of SomeNameHandler(object source, SomeEventArgs args) where source is the sender of the event and args is of the type EventArgs. The next step is to define our custom class that extends from EventArgs as in:

        public class MortgageEventArgs : EventArgs
        {
            public readonly Mortgage.Calculation calculation;
            public MortgageEventArgs(Mortgage.Calculation calculation)
            {
                this.calculation = calculation;
            }
        }

This class adds state to the base class EventArgs. The Calculation structure in turn is defined as:

        public struct Calculation
        {
            private double[] arrayDb;
            private int target;
            private string message;
            // populates data structure, does not validate data
            public Calculation(double principal, 
				double interest, 
				int months, 
				double payment, 
				int target, 
				string message)
            {...}
	}

So the MortgageEventArg contains an immutable Calculation structure that is passed to the Views along with a reference to the sender of the Event, the Controller. The Views can extract information from the Calculation structure and then update the presentation in the View.

The Controller class must provide a method of registering Views that wish to be notified when the Model state changes. .Net provides a built in mechanism for registering listeners using the event keyword. The event keyword creates add, remove and invoke semantics for Delegates much as Properties provide get and set logic for instance field variables. We magically add +=, -= and invoke semantics using the event keyword as in:

public event Mortgage.NotifyHandler NotifyDispatcher;

Thats it! Add, Remove and Invoke semantics are now automatically provided for the Delegate NotifyHandler through the variable NotifyDispatcher.  

Note: It is important that this declaration is made in the Controller class, since the Controller is the sender of the events. 

Any view that wishes to register for NotifyHandler events can do this using the += semantics as in:

controller.NotifyDispatcher += new Mortgage.NotifyHandler(Form1_OnINotifyEvent);

where Form1_OnINotifyEvent is the method in the View that will implement the type NotifyHandler as in:

        public void Form1_OnINotifyEvent(object source, Mortgage.MortgageEventArgs arg)
        {
            Mortgage.Calculation mortgage = arg.calculation;
            // struct cannot be null!
            if (mortgage.IsValid())
            {
                textBoxPrincipal.Text = mortgage.Principal.ToString();
                textBoxInterest.Text = mortgage.Interest.ToString();
                textBoxMonths.Text = mortgage.Months.ToString();
                textBoxPayment.Text = mortgage.Payment.ToString();
                textBoxMessage.Text = mortgage.Message.ToString();
                return;
            }
            else
            {
                textBoxMessage.Text = mortgage.Message.ToString();
                ResetControls();
                return;
            }
        }

This matches the Delegate declaration:

public delegate void NotifyHandler(object source, MortgageEventArgs args);

Each View can provide a different implementation of the method call, polymorphism at work! When the sender wishes to notify all registered Views it uses the NotifyDispatcher variable as in:

NotifyDispatcher(this, new Mortgage.MortgageEventArgs(mc));

As best as I can tell the compiler does something like this behind the scenes:

Delegate[] handlers= NotifyDispatcher.GetInvocationList();
foreach (Delegate d in handlers)
{
d.DynamicInvoke(
new object[]{this,
   
new Mortgage.MortgageEventArgs(mc)});
}

Note: If no listeners are registered to listen for an event, the event variable will be null and an attempt to fire events using the dispatcher will throw a null reference exception! Moreover, according to E. Gunderson, there is a possibility of a race condition if one writes:

if (Click != null) Click(this, arg);

So prefer:

     ClickHandler handler = Click;

     if (handler != null) handler(this, arg);

Events may also default to a lock on this. More at Skeet. As long as an event dispatcher holds a reference to a listener, the listener is not eligible for garbage collection (unless both the dispatcher and listener become unreachable). This is the source of the lapsed listener memory leak.

That's all folks! We declared a Delegate, automagically added add, remove and invoke semantics using the event keyword, registered listeners using the += semantics and invoked the invocation list. As a result, all registered listeners, Views, are notified of the change in the Model state. Since we programmed to a Type, each View is free to provide its own implementation of the NotifyHandler method.

View Adapters

I hesitate to add any more complexity to this project, but I think it is worth it to push the envelope and introduce the concept of a one to one Adapter. It is possible to bind a single View component to an instance of an inner class. This inner class acts as an Adapter, adapting the state of the Model to a specific state for the View component on a one on one basis. On the most simple level, the adapter could simply extract a single value from the Model and display it in a TextField, thus "binding" the TextField to single value in the Model. Here is a slightly more useful inner class that calculates the total cost of the mortgage over the life of the mortgage from the state of the Model:

        public class MortgageTotal 
        {
            TextBox tb;
            public MortgageTotal(TextBox tb)
            {
                this.tb = tb;
            }
            public Mortgage.NotifyHandler GetNotifyHandler()
            {
                return new Mortgage.NotifyHandler(MortgageTotal_OnNotifyEvent);
            }
            public void MortgageTotal_OnNotifyEvent(object source, 
							Mortgage.MortgageEventArgs arg)
            {
                Mortgage.Calculation c = arg.calculation;
                if ((tb != null) && c.IsValid())
                {
                    double total = c.Payment * c.Months;
                    tb.Text = String.Format("${0:N}",total);
                }
            }
        }

The user creates an instance of this class and passes in a reference to an existing TextBox as in:

MortgageTotal total = new MortgageTotal(textBoxTotal);

Note that the MortgageTotal class provides not only a function that matches the signature of the Delegate NotifyHandler, but also provides a method that returns an instance of the Delegate or a pointer to a concrete method (Remember, the Delegate is a type safe pointer to some method.) The application can use this instance of the Delegate when it wishes to register an interest with the Controller event. 

controller.NotifyDispatcher += total.GetNotifyHandler();

The Adapter adds yet another level of indirection between the View component, in this case a TextBox, and the Controller. This level of indirection adds both complexity and power. The power of the Adapter is more evident for a custom drag and drop visual component. It allows the user to bind a custom complex visual component to the state of the Model, adapting the interface of the visual component with the interface of the Model.

       

Learn More

MSDN: Events and Delegates

MSDN: Delegates vs Interfaces

Wikipedia: MVC

ModelViewPresenter

Well here is the code. Have fun!

View (Form1) Class

In this project, we still use the standard M-VC architecture to build the user interface. We create the Controller and inner class instance MortgageTotal. We then register an interest in Controller Events in the Form1 constructor:

        Controller controller = new Controller();
        public Form1()
        {
            InitializeComponent();
            MortgageTotal total = new MortgageTotal(textBoxTotal);
            // this listener registers an interest in NotifyHandler events from Controller
            controller.NotifyDispatcher += new Mortgage.NotifyHandler(Form1_OnINotifyEvent);
            controller.NotifyDispatcher += total.GetNotifyHandler();
        }

In the ButtonCalculate_Click event handler, we validate input and then call controller.Update.

            controller.Update(new Mortgage.Parameters(principal, 
							interest, 
							months, 
							payment, 
							target));

The Controller will then indirectly update the Views using Events.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace M_V_C
{
    /// <summary>
    /// Project demonstrates grafting of M-V-C on
    /// M-VC C# Windows Form Program
    /// 1) Modified our Mortgage Calculator to use immutable structures for thread safety
    /// 2) Abstracts interaction between Model and Controller,
    /// 3) Abstracts interaction between Controller and View
    /// allowing substitution/reuse of Model and View code
    /// 
    /// All interactions between View and Model go through the Controller
    /// so Model and View have no implementation knowledge of each other.
    /// Model implements Mortgage.ICalculate
    /// Controller interacts with the Model via ICalculate interface
    /// View registers for NotifyHandler events from the Controller
    /// Controller fires NotifyHandler events when Model state changes
    /// Controller implements Mortgage.IUpdate
    /// View calls Controller.IUpdate as needed to request changes in Model
    /// View passes MortgageParameter struct to Controller via IUpdate
    /// Controller passes MortgageEventArg to View via NotifyHandler event
    /// MortgageEventArg in turn, contains a MortgageCalculation struct
    /// </summary>
    public partial class Form1 : Form
    {
        Controller controller = new Controller();
        public Form1()
        {
            InitializeComponent();
            MortgageTotal total = new MortgageTotal(textBoxTotal);
            // this listener registers an interest in NotifyHandler events from Controller
            controller.NotifyDispatcher += new Mortgage.NotifyHandler(Form1_OnINotifyEvent);
            controller.NotifyDispatcher += total.GetNotifyHandler();
        }
        // Here is the actual handler that responds to NotifyHandler events from Controller
        public void Form1_OnINotifyEvent(object source, Mortgage.MortgageEventArgs arg)
        {
            Mortgage.Calculation mortgage = arg.calculation;
            // struct cannot be null!
            if (mortgage.IsValid())
            {
                textBoxPrincipal.Text = mortgage.Principal.ToString();
                textBoxInterest.Text = mortgage.Interest.ToString();
                textBoxMonths.Text = mortgage.Months.ToString();
                textBoxPayment.Text = mortgage.Payment.ToString();
                textBoxMessage.Text = mortgage.Message.ToString();
                return;
            }
            else
            {
                textBoxMessage.Text = mortgage.Message.ToString();
                ResetControls();
                return;
            }
        }
        private void buttonCalculate_Click(object sender, System.EventArgs e)
        {
            double principal = 0;
            double interest = 0;
            int months = 0;
            double payment = 0;
            bool isInputError = false;
            int target = Mortgage.INVALID;
            double[] arrayDb = new double[4];
            // validate user input, must allow zero
            try
            {
                principal = Double.Parse(textBoxPrincipal.Text);
                if (principal < 0)
                {
                    throw new Exception();
                }
                arrayDb[Mortgage.PRINCIPAL] = principal;
            }
            catch
            {
                textBoxPrincipal.Text = "Invalid Input.";
                isInputError = true;
            }
            try
            {
                interest = Double.Parse(textBoxInterest.Text);
                if ((interest < 0) || (interest > 100))
                {
                    throw new Exception();
                }
                arrayDb[Mortgage.INTEREST] = interest;
            }
            catch
            {
                textBoxInterest.Text = "Invalid Input.";
                isInputError = true;
            }
            try
            {
                months = Int32.Parse(textBoxMonths.Text);
                if (months < 0)
                {
                    throw new Exception();
                }
                arrayDb[Mortgage.MONTHS] = months;
            }
            catch
            {
                textBoxMonths.Text = "Invalid Input.";
                isInputError = true;
            }
            try
            {
                payment = Double.Parse(textBoxPayment.Text);
                if (payment < 0)
                {
                    throw new Exception();
                }
                arrayDb[Mortgage.PAYMENT] = payment;
            }
            catch
            {
                textBoxPayment.Text = "Invalid Input.";
                isInputError = true;
            }
            if (isInputError)
            {
                return;
            }
            // one, and only one, "value" must be zero --> target
            int zeros = 0;
            for (int index = 0; index < 4; index++)
            {
                if (arrayDb[index] == 0)
                {
                    zeros++;
                    target = index;
                }
            }
            if (zeros > 1)
            {
                textBoxMessage.Text = "Too many zero parameters.";
                isInputError = true;
                return;
            }
            if (zeros == 0)
            {
                textBoxMessage.Text = "One value must be zero.";
                isInputError = true;
                return;
            }
            // valid user input
            // abstract interaction View --> Controller
            // Controller must implement Mortgage.IUpdate.Update
            controller.Update(new Mortgage.Parameters(principal, 
							interest, 
							months, 
							payment, 
							target));
        }
        private void buttonClear_Click(object sender, System.EventArgs e)
        {
            textBoxMessage.Text = "";
            ResetControls();
        }
        private void ResetControls()
        {
            textBoxPrincipal.Text = "";
            textBoxInterest.Text = "";
            textBoxMonths.Text = "";
            textBoxPayment.Text = "0";
            textBoxPrincipal.Focus();
        }
        // inner class
        public class MortgageTotal 
        {
            TextBox tb;
            
            public MortgageTotal(TextBox tb)
            {
                this.tb = tb;
            }
            public Mortgage.NotifyHandler GetNotifyHandler()
            {
                return new Mortgage.NotifyHandler(MortgageTotal_OnNotifyEvent);
            }
            public void MortgageTotal_OnNotifyEvent(object source, 
							Mortgage.MortgageEventArgs arg)
            {
                Mortgage.Calculation c = arg.calculation;
                if ((tb != null) && c.IsValid())
                {
                    double total = c.Payment * c.Months;
                    tb.Text = String.Format("${0:N}",total);
                }
            }
        }
    } // end class Form1

}

Controller Class

The Controller class is pretty lightweight since the registration and invoke semantics are auto-magically added with the event keyword.

public event Mortgage.NotifyHandler NotifyDispatcher;

The extra level of indirection allows use to add logic between the call to Update and the notification of View components. In this case, we validate the result of the calculation and decide to do nothing if the calculation is invalid. We could also decide to store the state of the last calculation in this class as a get only property.

namespace M_V_C
{
    using System;
    // indirection between View and Model
    // View and Model have no implementation knowledge or each other
    // Views register an interest in NotifyHandler events via NotifyDispatcher
    // Request to change state of Model are triggered by call to IUpdate.Update
    public class Controller : Mortgage.IUpdate
    {
        // Controller owns the Model
        Model model = new Model();
        // create dispatcher to easily register interest in above event
        // eg. listener calls 
	// controller.NotifyDispatcher  += new Controller.NotifyHandler(Form1_OnNotifyEvent);
        // to register an interest in this event
        public event Mortgage.NotifyHandler NotifyDispatcher;
        public bool Update(Mortgage.Parameters mp)
        {
            Mortgage.Calculation mc = model.Calculate(mp);
            if (mc.IsValid())
            {
                // fire event to all registered listeners of change im model state
                NotifyDispatcher(this, new Mortgage.MortgageEventArgs(mc));
                return true;
            }
            else return false; // do nothing if input data is invalid
        }
    }
}

Model Class

Here again is the Model class from our 1997 program, modified to use immutable structures. Input is in the form of a Mortgage.Parameters struct and output is in the form of a Mortgage.Calculation struct.

        public Mortgage.Calculation Calculate(Mortgage.Parameters m)
        {
            return Calculate(m.Principal, m.Interest, m.Months, m.Payment, m.Target);
        }

The Mortgage.Parameters and Mortgage.Calculation structs are defined in a separate abstract Mortgage class.

namespace M_V_C
{
    using System;
    // Model that Encapsulate algorithms here
    // Cannot use static methods to implement interface so this is a class
    // Abstract interaction with controller by implementing Morgtage.ICalculate.Calculate
    public class Model : object, Mortgage.ICalculate
    {
        /// <summary>
        /// Class Model.cs
        /// jlouie 07.07.02
        /// updated 10.31.06 to return immutable Mortgage struct
        /// Mortgage struct is nested in this class
        /// Adapted from Model.java
        /// "Visual Cafe for Java Explorer, Database Development Edition"
        /// William Brogden, Jeffrey A. Louie, and Ed Tittel, Coriolis, 1998, 585pp.
        /// Supplied "as is"
        /// No warranty is expressed or implied
        /// This code is for instructional use only
        /// </summary>
        /// 
        // Model.GetCalculation returns immutable Mortgage structure
        // takes immutable Parameters structure
        // the actual amortization algorithm
        // m= P*i(1-(1+i)^-N)
        // i=r/1200
        // result= 0 --> marks error 
        public Mortgage.Calculation Calculate(Mortgage.Parameters m)
        {
            return Calculate(m.Principal, m.Interest, m.Months, m.Payment, m.Target);
        }
        private Mortgage.Calculation Calculate(double principal, 
			double interest, 
			int months, 
			double payment, 
			int target)
        {
            double[] arrayDb = new double[4];
            string message = "Invalid Result";
            // store input into array of double
            arrayDb[Mortgage.PRINCIPAL] = principal;
            arrayDb[Mortgage.INTEREST] = interest;
            arrayDb[Mortgage.MONTHS] = (double)months;
            arrayDb[Mortgage.PAYMENT] = payment;
            // clear target if target is valid
            if ((target >= Mortgage.PRINCIPAL) && (target <= Mortgage.PAYMENT))
            {
                arrayDb[target] = 0;
            }
            else // return on error
            {
                target = Mortgage.INVALID;
                message = "Invalid target.";
                return new Mortgage.Calculation(arrayDb[Mortgage.PRINCIPAL], 
			arrayDb[Mortgage.INTEREST], 
			(int)arrayDb[Mortgage.MONTHS], 
			arrayDb[Mortgage.PAYMENT], 
			Mortgage.INVALID, message);
            }
            // validate input
            // one, and only one, "value" must be zero --> target
            int zeros = 0;
            for (int index = 0; index < 4; index++)
            {
                if (arrayDb[index] == 0)
                {
                    zeros++;
                    target = index;
                }
            }
            if (zeros > 1)
            {
                message = "A necessary parameter is zero.";
                return new Mortgage.Calculation(arrayDb[Mortgage.PRINCIPAL], 
				arrayDb[Mortgage.INTEREST], 
				(int)arrayDb[Mortgage.MONTHS], 
				arrayDb[Mortgage.PAYMENT], 
				Mortgage.INVALID, message);
            }
            // validate interest
            if (interest > 100 || interest < 0)
            {
                message = "Invalid interest.";
                return new Mortgage.Calculation(arrayDb[Mortgage.PRINCIPAL], 
				arrayDb[Mortgage.INTEREST], (
				int)arrayDb[Mortgage.MONTHS], 
				arrayDb[Mortgage.PAYMENT], 
				Mortgage.INVALID, message);
            }
            // validate months
            if (months < 0)
            {
                message = "Invalid months.";
                return new Mortgage.Calculation(arrayDb[Mortgage.PRINCIPAL], 
				arrayDb[Mortgage.INTEREST], 
				(int)arrayDb[Mortgage.MONTHS], 
				arrayDb[Mortgage.PAYMENT], 
				Mortgage.INVALID, message);
            }
            // validate principal
            if (principal < 0)
            {
                message = "Invalid principal.";
                return new Mortgage.Calculation(arrayDb[Mortgage.PRINCIPAL], 
				arrayDb[Mortgage.INTEREST], 
				(int)arrayDb[Mortgage.MONTHS], 
				arrayDb[Mortgage.PAYMENT], 
				Mortgage.INVALID, message);
            }
            // validate payment
            if (payment < 0)
            {
                message = "Invalid payment.";
                return new Mortgage.Calculation(arrayDb[Mortgage.PRINCIPAL], 
			arrayDb[Mortgage.INTEREST], 
			(int)arrayDb[Mortgage.MONTHS], 
			arrayDb[Mortgage.PAYMENT], 
			Mortgage.INVALID, message);
            }
            // input parameters appear valid
            // pre-conditions have been met
            // target is valid
            double result = 0;
            double monthlyInterest = arrayDb[Mortgage.INTEREST] / 1200;
            try
            {
                switch (target)
                {
                    case Mortgage.PRINCIPAL:  // principal
                        result = 1 + monthlyInterest;
                        result = 1 / Math.Pow(result, arrayDb[Mortgage.MONTHS]);
                        result = ((1 - result) / monthlyInterest * arrayDb[Mortgage.PAYMENT]);
                        break;
                    case Mortgage.INTEREST:  // annual interest
                    // algorithm fails if 
		    // arrayDb[Model.MONTHS]*arrayDb[Model.PAYMENT] >= arrayDb[Model.PRINCIPAL]
                        if ((arrayDb[Mortgage.MONTHS] * arrayDb[Mortgage.PAYMENT]) 
								< arrayDb[Mortgage.PRINCIPAL])
                        {
                            throw new ArithmeticException();
                        }
                        // factor out Interest function, too long
                        result = CalcInterest(arrayDb[Mortgage.PRINCIPAL], 
				arrayDb[Mortgage.MONTHS], 
				arrayDb[Mortgage.PAYMENT]);
                        break;
                    case Mortgage.MONTHS:  // loan period
                        result = (1 - (arrayDb[Mortgage.PRINCIPAL] * (monthlyInterest) 
								/ arrayDb[Mortgage.PAYMENT]));
                        result = Math.Log(result);
                        result = -result / Math.Log((1 + monthlyInterest));
                        break;
                    case Mortgage.PAYMENT:  // monthly payments
                        result = 1 + monthlyInterest;
                        result = 1 / Math.Pow(result, arrayDb[Mortgage.MONTHS]);
                        result = (arrayDb[Mortgage.PRINCIPAL] * monthlyInterest) / (1 - result);
                        break;
                    //default:
                }
            }
            catch
            {
                result = 0;
            }
            // validate result
            // post-conditions
            if (Double.IsNaN(result))
            {
                result = 0;
            }
            if (result == 0)
            {
                message = "Input Error.";
            }
            else // valid result
            {
                arrayDb[target] = result;
                message = "";
            }
            return new Mortgage.Calculation(arrayDb[Mortgage.PRINCIPAL], 
				arrayDb[Mortgage.INTEREST], 
				(int)arrayDb[Mortgage.MONTHS], 
				arrayDb[Mortgage.PAYMENT], 
				target, message);
        } // end Calculation method
        // a complex iterative calculation for interest
        // thanks to Dr. W. Carlini (and Newton)for the solution
        // returns zero on error
        // ASSERT (N*m)>=P
        private double CalcInterest(double P, double N, double m)
        {
            double 	temp = (m / P), 
			answer = (m / P), 
			diff = 100, 
			numerator = 0, 
			denominator = 0,
        		accuracy = .00001;
            int index, maxIterations = 1000;
            try
            {
                for (index = 0; ((diff > accuracy) && (index < maxIterations)); index++)
                {
                    temp = answer;
                    numerator = (P * temp / m) + Math.Pow((1 + temp), -N) - 1;
                    denominator = (P / m) - N * Math.Pow((1 + temp), (-N - 1));
                    // if (denominator ==0 ){throw new ArithmeticException();}
                    answer = temp - (numerator / denominator);
                    diff = answer - temp;
                    if (diff < 0)
                    {
                        diff = -diff;
                    }
                }
                answer *= 1200;
                // validate answer
                if ((answer < 0) 
			|| Double.IsNaN(answer) 
			||(index == maxIterations))
                {
                    throw new ArithmeticException();
                }
            }
            catch
            {
                answer = 0;
            }
            return answer;
        } // end CalcInterest
    }// end Model
}

Mortgage Abstraction Class

Here is our abstract class that defines the class constants, structures, interfaces and delegates used in our project. We define the NotifyHandler delegate in this class, but we declare the event NotifyDelegate in the Controller, since the Controller is the sender of the events.

public delegate void NotifyHandler(object source, MortgageEventArgs args);
namespace M_V_C
{
    using System;
    // class used to abstract Mortgage algorithm (Model)
    // and commumication between View and Controller
    // not meant to be instantiated
    public abstract class Mortgage : Object
    {
        // static class constants, not "versionable"
        // must recompile to update
        public const int INVALID = -1;  // flags validation error
        public const int PRINCIPAL = 0;
        public const int INTEREST = 1;
        public const int MONTHS = 2;
        public const int PAYMENT = 3;
        public delegate void NotifyHandler(object source, MortgageEventArgs args);
        // Form/System request changes to Model via controller.Update
        // Controller should implement IUpdate
        public interface IUpdate
        {
            bool Update(Mortgage.Parameters mp);
        }
        // Controller could Notify View of change in Model state
        // vis INotify.Notify
        // This interface is not used in this example as Notification is
        // done via events between Controller and interested listeners
        // (Views)
        public interface INotify
        {
            bool Notify(Mortgage.Calculation mc);
        }
        // Model does calculation based on Parameter struct and
        // returns Calculation struct
        public interface ICalculate
        {
            Mortgage.Calculation Calculate(Mortgage.Parameters mp);
        }
        /// <summary>
        /// CALCULATION struct
        /// Immutable value type used to pass data between
        /// Model and Views via Controller 
        /// Should be thread safe
        /// Struct DOES NOT validate data, only checks for valid target
        /// Returns 0 values on invalid target
        /// </summary>
        public struct Calculation
        {
            private double[] arrayDb;
            private int target;
            private string message;
            // populates data structure, does not validate data
            public Calculation(double principal, 
				double interest, 
				int months, 
				double payment, 
				int target, 
				string message)
            {
                arrayDb = new double[4];
                // reset flags
                this.target = target;
                this.message = message;
                arrayDb[PRINCIPAL] = principal;
                arrayDb[INTEREST] = interest;
                arrayDb[MONTHS] = months;
                arrayDb[PAYMENT] = payment;
            }
            // default target is -1 (INVALID)
            public bool IsValid()
            {
		//double total= arrayDb[0]+arrayDb[1]+arrayDb[2]+arrayDb[3];
                //if (total == 0) { return false; } // no arg construction
                return ((target >= PRINCIPAL) && (target <= PAYMENT)) ? true : false;
            }
            public double Result
            {
                get
                {
                    return IsValid() ? arrayDb[target] : 0.0;
                }
            }
            public int Target
            {
                get
                {
                    return target;
                }
            }
            public string Message
            {
                get
                {
                    return message;
                }
            }
            public double Principal
            {
                get
                {
                    return IsValid() ? arrayDb[PRINCIPAL] : 0.0;
                }
            }
            public double Interest
            {
                get
                {
                    return IsValid() ? arrayDb[INTEREST] : 0.0;
                }
            }
            public int Months
            {
                get
                {
                    return IsValid() ? (int)arrayDb[MONTHS] : 0;
                }
            }
            public double Payment
            {
                get
                {
                    return IsValid() ? arrayDb[PAYMENT] : 0.0;
                }
            }
        }  // end Calculation Struct
        /// <summary>
        /// PARAMETERS Struct
        /// Immutable struct passed from Controller to Model
        /// and between View/Systmem and Controller
        /// Should be thread safe
        /// </summary>
        public struct Parameters
        {
            public readonly double Principal;
            public readonly double Interest;
            public readonly int Months;
            public readonly double Payment;
            public readonly int Target;

            public Parameters(double principal, 
				double interest, 
				int months, 
				double payment, 
				int target)
            {
                Principal = principal;
                Interest = interest;
                Months = months;
                Payment = payment;
                Target = Mortgage.INVALID;
                if ((target >= PRINCIPAL) && (target <= PAYMENT))
                {
                    Target = target;
                }
            }
        }
        public class MortgageEventArgs : EventArgs
        {
            public readonly Mortgage.Calculation calculation;
            public MortgageEventArgs(Mortgage.Calculation calculation)
            {
                this.calculation = calculation;
            }
        }
    } // end class Mortgage
}

A Better Mousetrap

As I try to expand on this amalgamation of M-VC and m_C_v it is not always clear what object is responsible for updating what control. To better "Delineate the Domain of Responsibility" I decided to create another View class called FormView that will be introduced in the next chapter. The FormView constructor looks like this:

        public FormView(TextBox textBoxPrincipal,
                                TextBox textBoxInterest,
                                TextBox textBoxMonths,
                                TextBox textBoxPayment,
                                TextBox textBoxMessage)

The FormView class implements the IViewable and IViewableEvents interface defined in Chapter 32.

-30-

 

Send mail to [email protected] with questions or comments about this web site. Copyright © 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 © 
Last modified: 08/04/09
Hosted by www.Geocities.ws

1