Wednesday, April 11, 2012

XAML App Review - Part 2: Form Manager

This is a continuation of the XAML App Review. The articles in this series are collected together here: Jeremy Bytes - Downloads.

Last time, we took a look at the overall operation and look and feel of the application UI.  This time, we'll look at the form manager -- how the different forms/pages are swapped in and out of the client area.  As a reminder, you can download the code or run the application here: Jeremy Bytes - Downloads.

ICloseQueryable Interface
There are a couple of features in WinForms that don't exist in the Silverlight world.  In the WinForms world, a form has a "Closing" event.  You can handle this event and issue a "Cancel" that will keep the form from closing.  If the user has unsaved data on the form, this gives you a chance to ask if the user wants to save or cancel changes before exiting.

The Silverlight world does not have a "Closing" event.  One reason is that in Silverlight we are primarily working with UserControls as opposed to forms.  For all intents and purposes, these can behave exactly as if they are forms, but there are a few differences.  One of these differences is the events that are available.

In the production application, each screen has an explicit save function (meaning data is not auto-saved). In order to make sure that the user had a chance to save before leaving a screen, I created an interface (ICloseQueryable) that is implemented on each screen where data can be modified.  Here's the interface (from the ICloseQueryable.cs file in the Common folder):

This is a very simple interface with just one method.  The name of the interface comes from the Delphi world (which our team used before we moved to .NET).

Here is a page that implements the interface ("Page 1"):

And the implementation (note: comments have been removed here for brevity):

Since this screen does not have an actual data object, the CanClose method is simply checking to see if the text in the text box has been changed from the default value.  If it has been changed, then the user is given a confirmation message box.  If the method returns "true", then the page is closed; otherwise, it stays open (we'll see this in just a bit).

In the real world, the CanClose method would check something from the business layer (such as a dirty flag) to see if there are unsaved changes.

The simple interface can be implemented in every form that needs to check for unsaved data before closing. Also, even though this wasn't the original intent, this could be used like a "Closing" event where we can do clean-up or finalization on the screen.

There is no guarantee that the CanClose method will be called.  This particular application has a design that ensures that this method gets called before a new screen is opened.  Also, as we'll see when we look at the form manager, the CanClose method does not get called when the application itself is shut down.  But this is also a problem with Silverlight in general because it runs in a browser, and there isn't a built-in mechanism to stop a user from closing the window.

This functionality may be better implemented as an event in a base form (UserControl) that all of the other forms descend from.  With an event, we would have better control over when it gets fired and ensuring that it gets fired.  I made a personal decision with this one.  I have been burned by UI frameworks that force the use of a base form in order for the application to work properly.  My native reaction is to take the simpler approach first and move toward the more complex one if necessary.  For this particular application, the interface solution is adequate.  For a more complex application, I would probably consider using a base form and events.

Another thing to consider: if we instead implemented an auto-save function for the data, then there is no need for the CanClose method or the interface.  Whether we implement auto-save is very dependent on the requirements of the application.

Form Management
There are a couple of different pieces to the form management in this application.  First, let's see the forms that we'll be working with:

The MainPage.xaml is the page that opens when the application starts.  It houses the menu and has a client area that is used for docking the sub-forms.  The CloseQuery1Page.xaml and CloseQuery2Page.xaml correspond with the "Page 1" and "Page 2" menu items.  The LoginPage.xaml is docked at start up (as we'll see in just a bit), and the other pages correspond with their menu items.

Let's start by taking a look at the form controller (this is in the FormController.cs file in the Common folder):

The idea behind the FormController class is that it maintains a list of forms that have been instantiated by the application.  If the application asks for a form (by calling GetForm), the form controller will check it's collection to see if that form already exists.  If it does exist, it will return that instance.  If it does not exist, then it will create an instance of the form, add it to its internal list, and then return that instance.

Notice that GetForm takes a parameter of type "Type".  This means that we will need to get the types somehow.  I do a bit of a cheat here: I set the Tag properties of each of the menu items (which are hyperlink buttons) to the corresponding type of the form.  This is done during the MainPage load process:

I'm not a big fan of using the Tag property for this type of thing, but it does get the job done.  As we'll see in just a bit, each of the hyperlink buttons is hooked up to a single event handler.  The Tag is used to instantiate the correct form.

As a side note, the "System.Windows.Browser.HtmlPage.Plugin.Focus" method makes sure that the Silverlight plugin has the focus (as opposed to the browser address bar or other item).

So, this event handler sets the tags of the menu items and then calls the ShowControl method with the LoginPage as a parameter.  The LoginPage constructor takes the type of another page.  When login is successful, this is the next page to navigate to (you can think of this as a "ReturnUrl" that you see when using ASP.NET forms authentication).  Here's what ShowControl looks like:

ShowControl takes a UIElement parameter (the same type used by the FormController).  This checks the ClientArea of the MainPage to see if it has at least one child.  If so, then it will remove that child and add a new child -- the UIElement that was passed in.

This code makes the assumption that there is only a single child of the ClientArea at any one time.  This is fine for this application, but the code should be made "safer" before reusing this.  This could be done by looping through all of the children and removing each one before adding a new child.

Finally, if the UIElement is the LoginPage, then it will set the focus to the text box for the login id.  This, in conjunction with the call to the Plugin.Focus above, ensures that the text box has the focus when the application starts.

The last piece of the puzzle is the click event for the hyperlink buttons.  As noted above, all of the buttons share the same event handler:

Here's how this works.  First, it gets the Tag from the hyperlink button that was clicked and stores it in the newType variable.  Next, it checks the current child of the ClientArea control to see if it implements ICloseQueryable.  If so, then this gets assigned to the oldForm variable.

_currentPage is a class-level variable that holds the type of the currently docked form.  If it is the same as the form that is requested (newType), then it exits.  This makes sure we don't do work to swap out forms when we are already on the requested form.

Next, it verifies that newType is populated (just in case the Tag property was not set properly).  Then it checks IsAuthenticated (a property that tells whether the user is authenticated).  If the user is not authenticated, then it redirects to the LoginPage and passes the requested page as the next page.

Next, it checks if the oldForm variable is populated -- this will only be populated if the previous form implements the ICloseQueryable interface.  It then calls the CanClose method.  If that method returns false (the user canceled the form close), then the application stays on the current form.

If none of the previous checks force a "return", then ShowControl is called.  Notice that the parameter for the method uses the FormController.GetForm method.  This will make sure that an existing instance is used if possible.  Finally, the _currentPage variable is set to the current form.

That's how it works; now let's do a bit of analysis.

It's fairly simple, and it works.  (I think "it works" is in the Pros column of pretty much any analysis).  One of the things that I like about this is that any UIElement can be used.  In our case, all of the forms are UserControls, but we could choose a different UIElement if it made sense.  Next, I like that we do not have to descend from a custom base class.  If the form does not implement "ICloseQueryable", then the code still works just fine.  If it does implement the interface, then we get the added functionality.

An advantage of the form controller is that we only instantiate one instance of each form.  This keeps the application from constantly creating and destroying the UserControl objects.

As noted above, we make assumptions about the number of children in the ClientArea.  Since ClientArea is a stack panel, it could have any number of children.  This assumption could cause potential problems.  I am never comfortable with the use of the Tag property.  It solves an issue for us since it allows all of the hyperlink buttons to easily share a single event handler, but it still doesn't "smell" right.  Finally, this methodology requires that all of the forms be statically compiled.  This is generally not an issue, but if we needed to dynamically add functionality (with new forms), we would need to rework this.

A disadvantage of the form controller is that we only have one instance of each form.  If we wanted to have multiple instances of a particular form (for example, multiple Orders open at the same time), then we would need to rework the form controller.  Also, if we re-open a form, it returns to its previous state.  This may or may not be what we want.  If we wanted to discard data, then we would need to reset the form somehow.

For this application, the solution is appropriate.  I would make some minor changes -- specifically, some of the names could be better.  "ICloseQueryable" and "FormController" could be renamed to something more appropriate; the same is true of a number of the variables.  "Form" is used in quite a few places, but "page" or "user control" may be better in the context.

Before using this in other applications, we would need to look at the requirements.  If we need to load forms dynamically, then we would need to rework this quite a bit.  Another option would be to look at the "Silverlight Navigation Application" template in Visual Studio.  We may even be able to use MEF (the Managed Extensibility Framework) depending on the situation.  There are tons of options out there.

Next Time
We took a look at a few more places where we can improve this application.  Fortunately, we haven't really run into a "that was a complete mistake" situation, but we may come across that as we continue our review.

Next time, we'll look at a Popup Message implementation.  This allows us to show messages to users that do not require user interaction.  This is a really cool feature that has quite a bit of room for enhancement.

Happy Coding!

No comments:

Post a Comment