The out of the box Caliburn.Micro WindowManager implementations in Silverlight do not seem to support multiple windows, so I'm trying to create a custom WindowManager to handle multiple windows in Silverlight 5.
I'm using the WPF implementation in Caliburn.Micro (which does handle multiple windows) as a starting point so I have the code below. Is this a good starting point?
It works in that it opens the required view in a new window. Unfortunately the line that I've prefixed with a comment and "/// >>>>" seems to cause an exception when I click any buttons in the resulting windows - it appears clicking any buttons seems to try and close the window and then throws an unhandled exception:
System.Exception: No target found for method MyMethod
If I change the ViewModelBinder.Bind method as follows, buttons in the resulting window\view call the correct methods on their viewmodel, but TryClose no longer works.
ViewModelBinder.Bind(rootModel, locatedView, context);
Can anybody suggest what is causing this? Or an alternate approach to implementing a multiple window manager for Silverlight 5 in Caliburn.Micro?
Code:
/// <summary>
/// Shows a window for the specified ViewModel.
/// </summary>
/// <param name="rootModel">The root ViewModel.</param>
/// <param name="context">The context.</param>
/// <param name="settings">The optional window settings.</param>
public virtual void ShowWindow(object rootModel, object context = null, IDictionary<string, object> settings = null)
{
// Some custom code - I've confirmed it's not causing any issues
CreateWindow(rootModel, false, context, settings).Show();
}
/// <summary>
/// Creates a window.
/// </summary>
/// <param name="rootModel">The view model.</param>
/// <param name="isDialog">Whethor or not the window is being shown as a dialog.</param>
/// <param name="context">The view context.</param>
/// <param name="settings">The optional settings.</param>
/// <returns>The window.</returns>
protected virtual Window CreateWindow(object rootModel, bool isDialog, object context, IDictionary<string, object> settings)
{
var locatedView = ViewLocator.LocateForModel(rootModel, null, context);
var view = EnsureWindow(rootModel, locatedView, isDialog);
/// >>>> The following binding seems to cause an exception. Binding to locatedView instead of view corrects this, but then TryClose doesn't work
ViewModelBinder.Bind(rootModel, view, context);
ApplySettings(view, settings);
new WindowConductor(rootModel, view);
return view;
}
/// <summary>
/// Makes sure the view is a window or is wrapped by one.
/// </summary>
/// <param name="model">The view model.</param>
/// <param name="view">The view.</param>
/// <param name="isDialog">Whethor or not the window is being shown as a dialog.</param>
/// <returns>The window.</returns>
protected virtual Window EnsureWindow(object model, object view, bool isDialog)
{
var window = view as Window;
if (window == null)
{
window = new Window
{
Content = (FrameworkElement) view
};
window.SetValue(View.IsGeneratedProperty, true);
}
return window;
}
static void ApplySettings(object target, IEnumerable<KeyValuePair<string, object>> settings)
{
if (settings == null)
return;
var type = target.GetType();
foreach (var pair in settings)
{
var propertyInfo = type.GetProperty(pair.Key);
if (propertyInfo != null)
propertyInfo.SetValue(target, pair.Value, null);
}
}
And this is the WindowConductor:
class WindowConductor
{
bool _actuallyClosing;
readonly Window _view;
readonly object _model;
public WindowConductor(object model, Window view)
{
_model = model;
_view = view;
var activatable = model as IActivate;
if (activatable != null)
{
activatable.Activate();
}
var deactivatable = model as IDeactivate;
if (deactivatable != null)
{
deactivatable.Deactivated += Deactivated;
}
var guard = model as IGuardClose;
if (guard != null)
{
view.Closing += Closing;
}
}
void Deactivated(object sender, DeactivationEventArgs e)
{
if (!e.WasClosed)
{
return;
}
((IDeactivate)_model).Deactivated -= Deactivated;
_actuallyClosing = true;
_view.Close();
_actuallyClosing = false;
}
void Closing(object sender, CancelEventArgs e)
{
if (e.Cancel)
{
return;
}
var guard = (IGuardClose)_model;
if (_actuallyClosing)
{
_actuallyClosing = false;
return;
}
bool runningAsync = false, shouldEnd = false;
guard.CanClose(canClose =>
{
Execute.OnUIThread(() =>
{
if (runningAsync && canClose)
{
_actuallyClosing = true;
_view.Close();
}
else
{
e.Cancel = !canClose;
}
shouldEnd = true;
});
});
if (shouldEnd)
{
return;
}
runningAsync = e.Cancel = true;
}
}
Additional details:
It looks like in the WPF version of Caliburn.Micro it successfully binds the viewmodel to a Window object, but in trying the same thing in Silverlight causes issues.
When I dig into the Caliburn.Micro code it looks like the following call in the default ActionMessage.SetMethodBinding:
currentElement = VisualTreeHelper.GetParent(currentElement);
Is not getting a Window as the parent of my View in Silverlight, but it does in WPF, and since the ViewModel is bound to the Window it never finds the viewmodel so doesn't call the actions.
I actually found later that changing CreateWindow to bind to locatedView (which is UserControl) instead of view (which is a Window wrapping a UserControl) actually works correctly in my main project, but not in the small sample sample project I was testing this in. I still don't understand why that is either, but haven't had a lot of time to dig into that.