-
Notifications
You must be signed in to change notification settings - Fork 0
First steps
Getting the first steps right is a lot easier, after understanding MVVM w/ Controller.
Disclaimer: The following example is wildly exaggerated - but this should show all the basic concepts in one example.
VM classes should be defined as abstract
(sealed
not!!), with a public, parameterless constructor to easily integrate with the vanilla framework:
using Caliburn.Micro;
using PropertyChanged; // https://github.com/Fody/PropertyChanged
[ImplementPropertyChanged]
public abstract class MainViewModel : <(I)Screen>
{
public string Name { get; set; }
public string Result { get; set; }
}
This down-side also has its benefits: No dependencies by the view model, which makes testing this layer nearly obsolete. (Who's going to test a state anyway?)
Additionally, you should not register your VM class in the vanilla container (Caliburn.Micro.IoC
, or Caliburn.Micro.SimpleContainer
and its extensions available on Caliburn.Micro.BootstrapperBase
)!
You can now define abstract
or virtual
methods for a convention-based invocation:
using Caliburn.Micro;
using PropertyChanged;
[ImplementPropertyChanged]
public abstract class MainViewModel : <(I)Screen>
{
public string Name { get; set; }
public string Result { get; set; }
public abstract void SayHello();
}
Thanks to some clever trick you don't have to implement IHandle<T>
, IHandleWithCoroutine<T>
, or IHandleWithTask<T>
to intercept published messages in your controller.
A Controller
is your universal and central unit for the application ('s logic):
using System.Threading.Tasks;
using Caliburn.Micro;
using Caliburn.Micro.Contrib.Controller;
using JetBrains.Annotations;
public sealed class MainController : ControllerBase<MainViewModel>
{
/// <remarks>Should be used to prepare <paramref name="screen" /></remarks>
public override void OnInitialize(MainViewModel screen)
{
base.OnInitialize(screen);
}
/// <remarks>Should be used to attach events</remarks>
public override void OnActivate(MainViewModel screen)
{
base.OnActivate(screen);
}
/// <remarks>Should be used to detach events</remarks>
public override void OnDeactivate(MainViewModel screen,
bool close)
{
base.OnDeactivate(screen,
close);
}
/// <remarks>Should be used for funky UI stuff (like initial validation, initial focus, ... stuff ... :beers:)</remarks>
public override async Task OnViewReadyAsync(MainViewModel screen,
object view)
{
await base.OnViewReadyAsync(screen,
view)
.ConfigureAwait(false);
}
public override void OnClose(MainViewModel screen,
bool? dialogResult = null)
{
base.OnClose(screen,
dialogResult);
}
}
Now, let's intercept void MainViewModel.SayHello()
:
using Caliburn.Micro;
using Caliburn.Micro.Contrib.Controller;
using JetBrains.Annotations;
public sealed class MainController : ControllerBase<MainViewModel>
{
[UsedImplicitly]
[InterceptProxyMethod(MethodName = nameof(MainViewModel.SayHello), CallBase = false)]
public void SayHello([NotNull] MainViewModel screen)
{
screen.Result = $"Hello {screen.Name}";
}
}
In this scenario, a simple interception won't suffice, so we are skipping the invocation on the view model with , CallBase = false
. You can transparently ensure the invocation on the view model with , CallBase = true
(default).
The above code is essentially the same as leaving MethodName = nameof(MainViewModel.SayHello)
out, as the annotated method's name is the fallback value for inserting the adapter between view model and controller.
Composition over inheritance
This approach gives you the possibility to write reusable and small units of code, which can be tested separately, to define common behavior for the View Model
:
using System.Threading.Tasks;
using Caliburn.Micro.Contrib.Controller.ControllerRoutine;
public sealed class DefaultDisplayNameRoutine : ControllerRoutineBase
{
/// <remarks>Should be used to prepare <paramref name="screen" /></remarks>
public override void OnInitialize(IScreen screen)
{
base.OnInitialize(screen);
}
/// <remarks>Should be used to attach events</remarks>
public override void OnActivate(IScreen screen)
{
base.OnActivate(screen);
}
/// <remarks>Should be used to detach events</remarks>
public override void OnDeactivate(IScreen screen,
bool close)
{
base.OnDeactivate(screen,
close);
}
/// <remarks>Should be used for funky UI stuff (like initial validation, initial focus, ... stuff ... :beers:)</remarks>
public override async Task OnViewReadyAsync(IScreen screen,
object view)
{
await base.OnViewReadyAsync(screen,
view)
.ConfigureAwait(false);
}
public override void OnClose(IScreen screen,
bool? dialogResult = null)
{
base.OnClose(screen,
dialogResult);
}
}
Now let's define some amazing logic for setting a default for Caliburn.Micro.IScreen.DisplayName
:
using System.Threading.Tasks;
using Caliburn.Micro.Contrib.Controller.ControllerRoutine;
public sealed class DefaultDisplayNameRoutine : ControllerRoutineBase
{
/// <remarks>Should be used for funky UI stuff (like initial validation, initial focus, ... stuff ... :beers:)</remarks>
public override async Task OnViewReady(IScreen screen,
object view)
{
await base.OnViewReadyAsync(screen,
view)
.ConfigureAwait(false);
screen.DisplayName = "Amazing default DisplayName";
}
}
Finally, define this as a constructor dependency of MainController
:
using Caliburn.Micro.Contrib.Controller;
using JetBrains.Annotations;
public sealed class MainController : ControllerBase<MainViewModel>
{
public MainController([NotNull] DefaultDisplayNameRoutine defaultDisplayNameRoutine)
: base(defaultDisplayNameRoutine)
}
Instead of making use of Caliburn.Micro.BootstrapperBase.DisplayRootViewFor
, we now interact with Caliburn.Micro.Contrib.Controller.Controller.ShowWindowAsync
or Caliburn.Micro.Contrib.Controller.Controller.ShowDialogAsync
:
using System;
using System.Collections.Generic;
using System.Windows;
using Caliburn.Micro;
using Caliburn.Micro.Contrib.Controller;
using JetBrains.Annotations;
public sealed class Bootstrapper : BootstrapperBase
{
public Bootstrapper()
{
this.Initialize();
}
[NotNull]
private SimpleContainer Container { get; } = new SimpleContainer();
protected override void Configure()
{
this.Container.Singleton<IWindowManager, WindowManager>();
this.Container.Singleton<IEventAggregator, EventAggregator>();
this.Container.Singleton<IControllerManager, ControllerManager>();
this.Container.Singleton<ILocator<ControllerBase>, Locator<ControllerBase>>();
this.Container.Singleton<ILocator<IWindowManager>, Locator<IWindowManager>>();
this.Container.Singleton<DefaultDisplayNameRoutine>();
this.Container.PerRequest<MainController>();
}
protected override void OnStartup(object sender,
StartupEventArgs e)
{
var controllerManager = IoC.Get<IControllerManager>();
controllerManager.ShowWindowAsync<MainController>(); // should be also called in other controllers, aka screen flow logic :beers:
}
// TODO protected override object GetInstance
// TODO protected override IEnumerable<object> GetAllInstances
// TODO protected override void BuildUp
}