JAL Computing

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

 

Home
Up

Chapter 32 "Add the IViewable and IViewableEvents Interface"

In this chapter I experiment further with grafting the m_C_v architecture onto a Windows Form application. 

First, to better delineate the Domain of Responsibility I have created a second View class called FormView so that there are now two View classes, FormView and MortgageTotalView. Each View class is responsible for maintaining the state of its contained controls in response to events.  

The FormView constructor looks like this:

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

The FormView instance does not contain a reference to the buttonCalculate component and therefore the FormView instance is not responsible for the state of buttonCalculate. Instead, Form1 remains responsible for maintaining the proper state of buttonCalculate when the user disables/enables calculations. This more complex implementation of the m-C-v architecture clearly separates the domain of responsibility.

Second, in order to generalize this solution I added two new interfaces that each View must implement, the IViewable and matching IViewableEvents interface. These interfaces are defined in the abstract class MCV. Note that the IViewableEvents interface defines methods that return concrete implementations of delegates for each event handler, enabling what I am going to call Supplier Driven Registration. This allows the Controller to register and unregister listeners to events, potentially limiting memory leaks due lapsed listeners. The Controller also fires the events. This supplier driven registration deviates from the standard .NET Observable pattern in which the supplier fires events and clients generally subscribe to the events. I am going to call this standard .NET Observable pattern Client Driven Registration, to differentiate it from the supplier driven registration scheme that follows.

using System;
using System.Collections.Generic;
using System.Text;
namespace M_C_V
{
    public abstract class MCV
    {
        public delegate void ClearHandler(object source, EventArgs e);
        public delegate void RefreshHandler(object source, EventArgs e);
        public delegate void EnableHandler(object source, EventArgs e);
        public delegate void DisableHandler(object source, DisableEventArgs e);
        public delegate void ResetHandler(object source, EventArgs e);
        public class DisableEventArgs : EventArgs
        {
            public readonly string Message = "";
            public DisableEventArgs(string message)
            {
                this.Message = message;
            }
        }
        public interface IViewable
        {
            bool Clear();
            bool Refresh();  // IViewable should store state for refresh
            bool Enable();
            bool Disable(string message);
            bool Reset(); // reset state to initial state
        }
        public interface IViewableEvents
        {
            DisableHandler GetDisableHandler();
            EnableHandler GetEnableHandler();
            ClearHandler GetClearHandler();
            RefreshHandler GetRefreshHandler();
            ResetHandler GetResetHandler();
        }
    } // end MCV class
}

Each View class supplies an implementation of the IViewable and IViewableEvents interfaces. This allows the Controller to register and fire the ClearHandler, ResetHandler, RefreshHandler, EnableHandler and DisableHandler events knowing that each View will respond appropriately. This also allows Form1 to directly invoke the Clear, Reset, Refresh, Enable and Disable methods on an individual View for a more granular level of control. Each View class stores the state of the last calculation to support the Refresh() function. In fact, it can be argued that the separation of responsibility into a separate View classes permits the storage of refresh state for the contained controls.

Note: The GetXXXHandler idiom can also be implemented using Generics and nested delegates as in:

public class TestInvoke<R, P> : IInvoke<R, P>
{
   
public delegate R DInvoke(P arg);
    public DInvoke GetInstanceDelegate() {
        return new TestInvoke<R,P>.DInvoke(this.Invoke);
    }

    public R Invoke(P arg)
    {
        System.Console.WriteLine(arg);
        return default(R);
   }
}

IViewable Interface

The IViewable interface is used to abstract common actions on a View. This is a generic interface that every View in this project should implement. Model specific behavior, on the other hand, is described by interfaces in the Mortgage class. Here again is the IViewable interface declaration:

        public interface IViewable
        {
            bool Clear();
            bool Refresh();  // IViewable should store state for refresh
            bool Enable();
            bool Disable(string message);
            bool Reset(); // reset non visible state to initial state
        }

Method Name: Clear()
Parameters:     None
Return Type:    bool
Assertions:       None
Description:    This method and the associated delegate ClearHander(...) is used to reset the visible state of an IViewable object to the default visible state. This method does not reset any non visible state, allowing the caller to restore the visible state of the IViewable object using the Refresh() method. The object should reset the visible state to its default visible state even if the IViewable object is disabled. This allows a Reset() and Clear() to return all IViewable objects to their default non visible and default visible state, even if some of the Views are disabled.

Method Name: Refresh()
Parameters:     None
Return Type:    bool
Assertions:       None
Description:     This method and the associated delegate RefreshHandler(..) is used to restore the visible state of an IViewable object based on the IViewable objects non visible state. If an internal error is encountered the Refresh() method may elect to call Clear() or report the error in the View. If the non visible state of the IViewable object has been immediately reset using the Reset() method, the visible state should be the default visible state. After a successive atomic call to Clear() and then Refresh(), the IViewable object should return to its state prior to the call to Clear(). One could think of the Refresh() method as a UndoClear() method, except that the Refresh() method should work even if the user has edited, but not saved, changes to the IViewable object. If the object is disabled, the object should not refresh the View, but may wish to return the View to its "disabled" state.

Method Name: Enable
Parameters:     None
Return Type:    bool
Assertions:       None
Description:     This method and the associated delegate EnableHandler(...) is used to enable an IViewable object. If the object is already enabled, no action is taken. If the object was disabled, the object is enabled but should be in its default state, both non visible and visible state.

Method Name: Disable()
Parameters:     string message
Return Type:    bool
Assertions:       None
Description:     This method and the associated delegate DisableHandler(...) is used to disable an IViewable object. If the object is already disabled, no action is taken. If the object was enabled, the object is disabled and returned to its default state, both non visible and visible. This may be accomplished by calling Clear() and Reset(). However, the visible state of the disabled View may differ from the visible state of an enabled View. For instance, the string parameter, message, may be used to communicate the disabled state to the user. The disable message may be part of the visible state of the IViewable object when it is disabled

Method Name: Reset()
Parameters:     None
Return Type:    bool
Assertions:       None
Description:     This method and the associated delegate ResetHandler(...) is used to restore the non visible state of an IViewable object to its default state. Use the Clear() method to restore the visible state of an IViewable object to its default state. To completely restore an IViewable object to its default state, use both the Reset() and Clear() methods. If the object is disabled, the View object non visible state should still be set to its default state. This allows a Reset() and Clear() to return all IViewable objects to their default non visible and default visible state, even if some of the Views are disabled.

 

MortgageTotalView Class

Here is the MortgageTotalView class that implements the IViewable and IViewableEvents interface. This class returns the appropriate  XXXXHandler in the appropriate GetXXXXHandler() method. Each XXXXHandler in IViewableEvents simply passes the call to the matching IViewable.XXXX method. For instance, the GetClearHandler() method simple returns a concrete implementation of the ClearHandler delegate.

        public MCV.ClearHandler GetClearHandler()
        {
            return new MCV.ClearHandler(MortgageTotalView_OnClearEvent);
        }

The actual ClearHandler implementation, the MortgageTotalView_OnClearEvent method, simple passes the call to IViewable.Clear() as in:

        public void MortgageTotalView_OnClearEvent(object source, EventArgs e)
        {
            this.Clear();
        }

The IViewable.Clear() method then clears the View.

        public bool Clear()
        {
            tb.Clear();
            return true;
        }

The caller can reach the Clear() method directly through IViewable.Clear() or indirectly by firing the ClearHandler event. 

Here is the actual class code:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
namespace M_C_V
{
    // VIEW CLASS to separate and delineate "domain of responsibility"
    /// <summary>
    /// MORTGAGE TOTAL Class
    /// class that calculate total mortgage and displays result in a textbox
    /// implements IViewable interface so it can respond to Clear,Enable,Disable,Refresh
    /// returns delegates from IViewableEvents that correspond to each method in IViewable
    /// each View must store local state for Refresh()
    /// </summary>
    public class MortgageTotalView : MCV.IViewable, MCV.IViewableEvents
    {
        private TextBox tb;
        private bool isEnabled = true;
        private string disableMessage = "";
        private Mortgage.Calculation defaultCalculation= new Mortgage.Calculation(0, 0, 0, 0, Mortgage.INVALID, "");
        private Mortgage.Calculation lastCalculation;
                        
        public Mortgage.Calculation Calculation
        {
            get { return this.lastCalculation; }
        }
        // Constructor
        // ASSERT tb != null
        public MortgageTotalView(TextBox tb) 
        {
            if (tb == null)
            {
                throw new ArgumentNullException();
            }
            this.tb = tb;
            this.lastCalculation = defaultCalculation;
        }
        // implement IViewable methods
        public bool Clear()
        {
            tb.Clear();
            return true;
        }
        // refresh from LOCAL data
        public bool Refresh()
        {
            if (lastCalculation.IsValid())
            {
                RefreshView(lastCalculation);
                return true;
            }
            else
            {
                this.Clear();
                return false;
            }
        }
        public bool Enable()
        {
            isEnabled = true;
            return true;
        }
        // reset and clear
        public bool Disable(string message)
        {
            isEnabled = false;
            if (message != null)
            {
                disableMessage = message;
            }
            else
            {
                disableMessage = "";
            }
            this.Reset();
            this.Clear();
            return true;
        }
        public bool Reset()
        {
            this.lastCalculation = defaultCalculation;
            return true;
        }
        // helper method for Refresh
        private void RefreshView(Mortgage.Calculation mc)
        {
            if (isEnabled)
            {
                if ((tb != null) && mc.IsValid())
                {
                    lastCalculation = mc;  // store last Mortgage.Calculation
                    double total = mc.Payment * mc.Months;
                    tb.Text = String.Format("${0:N}", total);
                }
                else
                {
                    tb.Text = "Error.";
                }
            }
            else
            {
                tb.Text = disableMessage;
            }
        }
        // Provide methods that return INSTANCES of delegates for IViewable handlers
        public Mortgage.NotifyHandler GetNotifyHandler()
        {
            return new Mortgage.NotifyHandler(MortgageTotalView_OnNotifyEvent);
        }
        public MCV.DisableHandler GetDisableHandler()
        {
            return new MCV.DisableHandler(MortgageTotalView_OnDisableEvent);
        }
        public MCV.EnableHandler GetEnableHandler()
        {
            return new MCV.EnableHandler(MortgageTotalView_OnEnableEvent);
        }
        public MCV.ClearHandler GetClearHandler()
        {
            return new MCV.ClearHandler(MortgageTotalView_OnClearEvent);
        }
        public MCV.RefreshHandler GetRefreshHandler()
        {
            return new MCV.RefreshHandler(MortgageTotalView_OnRefreshEvent);
        }
        public MCV.ResetHandler GetResetHandler() 
        {
            return new MCV.ResetHandler(MortgageTotalView_OnResetEvent);
        }
        // Actual event handlers
        public void MortgageTotalView_OnNotifyEvent(object source, Mortgage.MortgageEventArgs e)
        {
            Mortgage.Calculation c = e.calculation;
            // restore from LOCAL data
            RefreshView(c);
        }
        public void MortgageTotalView_OnDisableEvent(object source, MCV.DisableEventArgs e)
        {
            this.Disable(e.Message);
        }
        public void MortgageTotalView_OnEnableEvent(object source, EventArgs e)
        {
            this.Enable();
        }
        public void MortgageTotalView_OnClearEvent(object source, EventArgs e)
        {
            this.Clear();
        }
        public void MortgageTotalView_OnRefreshEvent(object source, EventArgs e)
        {
            this.Refresh();
        }
        public void MortgageTotalView_OnResetEvent(object source, EventArgs e)
        {
            this.Reset();
        }
    } // end MortgageTotalView Class
}

FormView Validates Pre-Conditions

The FormView object provides a method GetParameters() that returns a ParametersBoolStruct. The ParametersBoolStruct is used to communicate pre-condition violations _and_ returns the user input as a Mortgage.Parameters struct.

public Mortgage.ParametersBoolStruct GetParameters()

The ParametersBoolStruct is defined as:

        // wraps bool result, message and source as value type
        // helper properties for bool result: B
        // immutable structure
        // use to return Mortgage.Parameters wrapped in boolean struct
        public struct ParametersBoolStruct
        {
            public readonly bool IsSuccess;
            public readonly String Message;
            public readonly String Source;
            public readonly Parameters Parameters;
            public ParametersBoolStruct(bool isSuccess, 
						String message, 
						String source, 
						Parameters parameters)
            {
                if (message == null) { message = ""; }
                if (source == null) { source = ""; }
                this.IsSuccess= isSuccess;
                this.Message = message;
                this.Source = source;
                this.Parameters = parameters;
            }
            public bool B // helper
            {
                get { return IsSuccess; }
            }
        }

The FormView validates user input and returns a Mortgage.Parameters structure embedded in a ParametersBoolStruct. The client, Form1, examines the ParametersBoolStruct to determine if the validation was successful. If so, the client, Form1, extracts the valid input parameters from Mortgage.Parameters.

            Mortgage.ParametersBoolStruct boolStruct = formView.GetParameters();
            if (boolStruct.IsSuccess && (boolStruct.Parameters.Target != Mortgage.INVALID))
            {
                controller.Update(boolStruct.Parameters);
            }

Note: In Chapter 36, examine the "try, out" version of GetParameters. I think this version is less complex to code and the single out parameter is unlikely to cause any confusion.  In the "try, out" version, the caller declares a variable of the proper type and passes this variable as a parameter to the TryGetParameters method explicitly using the out keyword as in:

            Mortgage.Parameters parameters;
            bool success= formView.TryGetParameters(out parameters); // caller must specify out
            if (success && (parameters.Target != Mortgage.INVALID))
            {
                controllerCurrent.Update(parameters);
            }

FormView Code

Here is the FormView class. Much of the complex logic has been moved out of the Form1 class and moved into this class. In fact, the Form1 class is now much smaller and contains mostly initiation code and menu event handling code.

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
namespace M_C_V
{
    // VIEW CLASS to separate and delineate "domain of responsibility"
    /// <summary>
    /// FORM VIEW Class
    /// class that displays parameters and result of calculations in textboxes
    /// implements IViewable interface so it can respond to Clear,Enable,Disable,Refresh
    /// returns delegates from IViewableEvents that correspond to each method in IViewable
    /// this class validates user input
    /// returning a Mortgage.ParametersBoolStruct to client, see GetParameters()
    /// each IViewable must store Calculation state to support Refresh(), see lastCalculation
    /// </summary>
    public class FormView : MCV.IViewable, MCV.IViewableEvents
    {
        private TextBox textBoxPrincipal;
        private TextBox textBoxInterest;
        private TextBox textBoxMonths;
        private TextBox textBoxPayment;
        private TextBox textBoxMessage;
        private bool isEnabled = true;
        private string disableMessage = "";
        private Mortgage.Calculation defaultCalculation =
                        new Mortgage.Calculation(0, 0, 0, 0, Mortgage.INVALID, "");
        private Mortgage.Calculation lastCalculation;

        // Constructor
        // Any control passed in here is in domain of responsibility of this class
        // this class is responsible for properly updating state of contained controls
        // in response to controller events
        // note buttonCalculate is NOT in domain of responsibility of this class
        public FormView(TextBox textBoxPrincipal,
                                TextBox textBoxInterest,
                                TextBox textBoxMonths,
                                TextBox textBoxPayment,
                                TextBox textBoxMessage)
        {
            this.textBoxPrincipal = textBoxPrincipal;
            this.textBoxInterest = textBoxInterest;
            this.textBoxMonths = textBoxMonths;
            this.textBoxPayment = textBoxPayment;
            this.textBoxMessage = textBoxMessage;
            this.lastCalculation = defaultCalculation;
        }
        public Mortgage.Calculation Calculation
        {
            get { return this.lastCalculation; }
        }
        // this class is responsible for validating
        // preconditions and communicating with client via ParametersBoolStruct
        public Mortgage.ParametersBoolStruct GetParameters()
        {
            double principal = 0;
            double interest = 0;
            int months = 0;
            double payment = 0;
            bool isInputError = false;
            int target = Mortgage.INVALID;
            Mortgage.Parameters invalidParameters = new Mortgage.Parameters(principal,
                                                            interest,
                                                            months,
                                                            payment,
                                                            Mortgage.INVALID);
            Mortgage.ParametersBoolStruct invalidStruct = new Mortgage.ParametersBoolStruct(false,
                                                            "Invalid Input",
                                                            "FormView",
                                                            invalidParameters);
            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 invalidStruct;
            }
            // 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 invalidStruct;
            }
            if (zeros == 0)
            {
                textBoxMessage.Text = "One value must be zero.";
                isInputError = true;
                return invalidStruct;
            }
            // success
            Mortgage.Parameters parameters = new Mortgage.Parameters(principal,
                                                       interest,
                                                       months,
                                                       payment,
                                                       target);
            return new Mortgage.ParametersBoolStruct(true, "", "FormView", parameters);
        }
        // implement IViewable methods
        public bool Clear()
        {
            textBoxMessage.Text = "";
            ClearControls();
            return true;
        }
        // restore data from LOCAL state
        public bool Refresh()
        {
            if (lastCalculation.IsValid())
            {
                RefreshView(lastCalculation);
                return true;
            }
            else
            {
                this.Clear();
                return false;
            }
        }
        public bool Enable()
        {
            isEnabled = true;
            return true;
        }
        // reset and clear
        public bool Disable(string message)
        {
            isEnabled = false;
            if (message != null)
            {
                disableMessage = message;
            }
            else
            {
                disableMessage = "";
            }
            this.Reset();
            this.Clear();
            return true;
        }
        public bool Reset()
        {
            this.lastCalculation = defaultCalculation;
            return true;
        }
        // helper method for Clear, Refresh
        private void ClearControls()
        {
            textBoxPrincipal.Text = "";
            textBoxInterest.Text = "";
            textBoxMonths.Text = "";
            textBoxPayment.Text = "0";
            textBoxPrincipal.Focus();
        }
        // private helper method for Refresh
        // works with data pushed from controller
        // works with data stored in this view
        private void RefreshView(Mortgage.Calculation mortgage)
        {
            // enable/disable logic
            if (isEnabled == false)
            {
                textBoxMessage.Text = disableMessage;
                ClearControls();
                return;
            }
            // struct cannot be null!
            if (mortgage.IsValid())
            {
                lastCalculation = mortgage;
                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();
                ClearControls();
                return;
            }
        }
        // INotifyHandler
        // data is pushed from Controller
        public void FormView_OnNotifyEvent(object source, Mortgage.MortgageEventArgs e)
        {
            if (isEnabled)
            {
                Mortgage.Calculation calculation = e.calculation;
                RefreshView(calculation);
            }
            else
            {
                textBoxMessage.Text = disableMessage;
                ClearControls();
            }
        }
        // Provide methods that return INSTANCES of delegates for IViewableEvent handlers
        public Mortgage.NotifyHandler GetNotifyHandler()
        {
            return new Mortgage.NotifyHandler(FormView_OnNotifyEvent);
        }
        public MCV.DisableHandler GetDisableHandler()
        {
            return new MCV.DisableHandler(FormView_OnDisableEvent);
        }
        public MCV.EnableHandler GetEnableHandler()
        {
            return new MCV.EnableHandler(FormView_OnEnableEvent);
        }
        public MCV.ClearHandler GetClearHandler()
        {
            return new MCV.ClearHandler(FormView_OnClearEvent);
        }
        public MCV.RefreshHandler GetRefreshHandler()
        {
            return new MCV.RefreshHandler(FormView_OnRefreshEvent);
        }
        public MCV.ResetHandler GetResetHandler()
        {
            return new MCV.ResetHandler(FormView_OnResetEvent);
        }
        // Actual event handlers, source is of class controller!
        public void FormView_OnDisableEvent(object source, MCV.DisableEventArgs e)
        {
            this.Disable(e.Message);
        }
        public void FormView_OnEnableEvent(object source, EventArgs e)
        {
            this.Enable();
        }
        public void FormView_OnClearEvent(object source, EventArgs e)
        {
            this.Clear();
        }
        public void FormView_OnRefreshEvent(object source, EventArgs e)
        {
            // restore from LOCAL data
            this.Refresh();
        }
        public void FormView_OnResetEvent(object source, EventArgs e)
        {
            // reset LOCAL data and clear controls
            this.Reset();
        }
    } // end FormView class
}

Download Code

Here is the latest project build: M_C_V.zip

User Privileges

In the next chapter, Chapter 33, we will enforce user privileges using three controllers. Since each controller only contains type safe pointers to the Views, there is only one set of View objects.

- 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