Thursday, October 14, 2010

Turning SilverlightFX into a true MVVM

The original SilverlightFX framework by Nikhil Kothari is a fabulous piece of software. It demonstrates how to create applications where the user interface can be realistically designed in Blend without (in most cases) requiring the use of any code-behind logic.

Currently I have melded this framework together with Prism, Unity, MEF and .NET4 to produce a new all-encompassing framework…

Views

One aspect which I chose to refactor was the code dealing with Views.

The primary motivation for this was a desire to create a custom control view that provided a basic property page. Secondly I wanted my property page to derive from the new ChildWindow control instead of the SilverlightFX Form.

What I really want is four different flavours of view each derived from a different point in the control hierarchy like this;

View – derived from Control

UserView – derived from UserControl

ChildView – derived from ChildWindow

ContentView – derived from ContentControl

The first problem with this approach is that there is already a ContentView in the SilverlightFX framework so the first task is to rename this to TransitioningContentControl which is perhaps a better description of what it does…

Next problem is that of the PageLoader and Controller classes which expect to talk directly to a View – no problem – we will create an IView interface and derive all our ViewXXX classes from it. To get the PageLoader to work we need to add a few methods to this interface.

public interface IView
{
bool IsEnabled
{
get;
set;
}

bool Focus();
void InitializeViewData(IDictionary<string, object> viewData);
}



The implementation of IsEnabled and Focus can be reused from the base implementation in Control.


The remaining method simply caches the dictionary – it is used in the OnLoaded event handler to initialise the view.


While we are here we will also revise the implementation of ViewModel so we can pass the IView object to the view-model during initialisation – this will allow the view-model to communicate with the view in an alternative way to properties and events.

public abstract class ViewModel : Model
{
public virtual void InitializeView(IView view)
{
}
}

Each View class defines an attached-property for storing the ViewModel (I’m sure I only need to do this once and in a single class but in the interests of getting it done I opted for the simplest route – please feel free to experiment!)


The constructor for each View registers for the Loaded event and provides a virtual OnLoaded method. This method is where the heavy lifting of view-model initialisation is performed. To avoid having four copies of the view-model initialisation code this has been refactored into its own class…

protected virtual void OnLoaded()
{
object viewModel = UserView.GetViewModel(this);
if (viewModel == null)
{
viewModel = ViewModelAttribute.CreateViewModel(this);
if (viewModel != null)
{
UserView.SetViewModel(this, viewModel);
}
}
ViewModelInitializer.InitializeModel(this, _viewData, viewModel);
}



The view model initializer class contains the original View code that initialised the view-model using reflection.


The ViewModelAttribute class is almost unchanged although some of the method signatures changed from UserControl to Control to accommodate the new hierarchy.


Controller and PageLoader


The controller and page-loader classes expect View classes and these all had to be changed to use IView instead – makes the code look much nicer in my humble opinion too!


Form


So you make all these changes and eventually the framework will compile again. We don’t get to rest for long because the next change is the one we need to make in order to support our new property page!


Currently the Form is derived from UserView (well it used to be View but to get the code to compile you have probably worked out it now needs to be derived from UserView!) however what would be really cool is to derive the Form from ChildView instead. If we can get this to work we will gain the advantage of using the .NET control for popup forms (which supports being moved, close buttons and a built-in title bar) plus we will be able to create custom controls derived from this and Visual Studio will be happy…


This change isn’t too bad – seems the two classes do play nicely together with a little help.


Here’s what I came up with…

    public class Form : ChildView
{
/// <summary>
/// Represents the CloseEffect property.
/// </summary>
public static readonly DependencyProperty CloseEffectProperty =
DependencyProperty.Register("CloseEffect", typeof(AnimationEffect), typeof(Form), null);

/// <summary>
/// Represents the ShowEffect property.
/// </summary>
public static readonly DependencyProperty ShowEffectProperty =
DependencyProperty.Register("ShowEffect", typeof(AnimationEffect), typeof(Form), null);

private bool _canClose;
private bool _pendingResult;
private bool _executedModelCommand;

private DelegateCommand<object> _cancelCommand;
private DelegateCommand<object> _okCommand;

/// <summary>
/// Gets or sets the effect to be played when the form is closing.
/// </summary>
public AnimationEffect CloseEffect
{
get
{
return (AnimationEffect)GetValue(CloseEffectProperty);
}
set
{
SetValue(CloseEffectProperty, value);
}
}

/// <summary>
/// Gets or sets the effect to be played when the form is being shown.
/// </summary>
public AnimationEffect ShowEffect
{
get
{
return (AnimationEffect)GetValue(ShowEffectProperty);
}
set
{
SetValue(ShowEffectProperty, value);
}
}

/// <summary>
/// Closes the Form with the specified FormResult code.
/// </summary>
/// <param name="result">The result of the Form.</param>
public void Close(bool result)
{
if (!_canClose)
{
return;
}

_canClose = false;

// Run the close effect if we have one otherwise close directly
AnimationEffect closeEffect = CloseEffect;
if (closeEffect == null)
{
// Setting dialog result will automatically close the dialog
// NOTE: I think this is a bug in the ChildWindow...
DialogResult = result;
}
else
{
// Cache the result - we will apply it when the anim finishes
_pendingResult = result;
if (((IAttachedObject)closeEffect).AssociatedObject != this)
{
((IAttachedObject)closeEffect).Attach(this);
closeEffect.Completed += OnCloseEffectCompleted;
}
closeEffect.PlayEffect(AnimationEffectDirection.Forward);
}
}

protected override void OnLoaded()
{
base.OnLoaded();

_cancelCommand = new DelegateCommand<object>(OnCancelCommand);
_okCommand = new DelegateCommand<object>(OnOKCommand);

Resources.Add("CancelCommand", _cancelCommand);
Resources.Add("OKCommand", _okCommand);
}

protected override void OnOpened()
{
// Play the opening effect if we have one
AnimationEffect showEffect = ShowEffect;
if (showEffect == null)
{
_canClose = true;
base.OnOpened();
}
else
{
_canClose = false;

if (((IAttachedObject)showEffect).AssociatedObject != this)
{
((IAttachedObject)showEffect).Attach(this);
showEffect.Completed += OnShowEffectCompleted;
}
showEffect.PlayEffect(AnimationEffectDirection.Forward);
}
}

protected override void OnClosing(CancelEventArgs e)
{
// If we haven't called into the model yet then do it now
if (!_executedModelCommand)
{
ExecuteModelCommand(DialogResult.HasValue ? DialogResult.Value : false);
e.Cancel = true;
return;
}

// Delegate to base class to raise event
base.OnClosing(e);
}

private void ExecuteModelCommand(bool result)
{
FormViewModel model = ChildView.GetViewModel(this) as FormViewModel;
if (model != null)
{
_executedModelCommand = true;
if (result)
{
model.Commit(
delegate()
{
if (model.HasCompleted)
{
if (DialogResult.HasValue)
{
Close();
}
else
{
_canClose = true;
Close(result);
}
}
else
{
// Completion was undone - we will not close
// at this time so re-enable the close action
// and ensure model command will be retried.
_canClose = true;
_executedModelCommand = false;
}
});
}
else
{
model.Cancel(
delegate()
{
if (DialogResult.HasValue)
{
Close();
}
else
{
_canClose = true;
Close(result);
}
});
}
return;
}

if (DialogResult.HasValue)
{
Close();
}
else
{
_canClose = true;
Close(result);
}
}

private void OnCancelCommand(object args)
{
ExecuteModelCommand(false);
}

private void OnOKCommand(object args)
{
ExecuteModelCommand(true);
}

private void OnShowEffectCompleted(object sender, EventArgs e)
{
_canClose = true;
base.OnOpened();
}

private void OnCloseEffectCompleted(object sender, EventArgs e)
{
// Setting dialog result will automatically close the dialog
// NOTE: I think this is a bug in the ChildWindow...
DialogResult = _pendingResult;
}
}



The open and close effect animation stuff should make a fair amount of sense – this is the code that SilverlightFX has that the ChildWindow does not.


The slightly smelly code is that required to call into the view-model – the SilverlightFX version had the advantage of access to methods that are private in the ChildWindow hence the is a little more state needed. If the user uses the close button we want to notify the view-model (obviously if the user uses the cancel command then we can be more explicit in calling the model)


The eagle-eyed among you will have noticed that I sneaked in a HasCompleted property into the view-model – the backing field for this property already exists in the FormViewModel class – I just provided a public getter fot it! It is added to enable the view-model to abort a commit and stop the close action from taking place – this is only supported during commit (not cancel).


The FormModel class gains the following method – this is a breaking behaviour change and means that handlers that connect to the Completed event of a FormModel must check whether the model actually completed before processing the IsCommitted flag…

        protected void CompleteUndo()
{
if (_completed)
{
throw new InvalidOperationException("Model has already been completed.");
}

if (_completedHandler != null)
{
_completedHandler(this, EventArgs.Empty);
}
if (_completeCallback != null)
{
_completeCallback();
}
}



This change will probably come back to bite me real soon…


That will do for now – I will present the TriStateFormModel and finally the EditorForm/EditorFormModel classes in the next episode – until then happy coding!


Oh these view changes have not been uploaded to CodePlex yet – my day job keeps getting in the way but it will happen soon and I’ll blog on here when it does!


Technorati Tags: ,,,

No comments:

Post a Comment