-
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 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([NotNull] MainViewModel screen)
{
base.OnInitialize(screen);
}
/// <remarks>Should be used to attach events</remarks>
public override void OnActivate([NotNull] MainViewModel screen)
{
base.OnActivate(screen);
}
/// <remarks>Should be used to detach events</remarks>
public override void OnDeactivate([NotNull] 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 void OnViewReady([NotNull] MainViewModel screen,
[NotNull] object view)
{
base.OnViewReady(screen,
view);
}
public override void OnClose([NotNull] 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]
[ScreenMethodLink(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.
Finally, you can also force an asynchronous execution of the controller's method by setting CallAsync = true
even if the screen's method is defined as synchronous:
using System.Threading.Tasks;
using Caliburn.Micro;
using Caliburn.Micro.Contrib.Controller;
using JetBrains.Annotations;
public sealed class MainController : ControllerBase<MainViewModel>
{
[UsedImplicitly]
[ScreenMethodLink(MethodName = nameof(MainViewModel.SayHello), CallAsync = true, CallBase = false)]
public async Task SayHelloAsync([NotNull] MainViewModel screen)
{
screen.Result = $"Hello {screen.Name}";
}
}
Caution: Defining the method's return-type as
Task<>
makes any innerawait
-call a possible deadlock scenario - To get around this, please useawait xxx.ConfigureAwait(false);
!
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 Caliburn.Micro.Contrib.Controller.ControllerRoutine;
public sealed class DefaultDisplayNameRoutine : ControllerRoutineBase
{
/// <remarks>Should be used to prepare <paramref name="screen" /></remarks>
public override void OnInitialize([NotNull] IScreen screen)
{
base.OnInitialize(screen);
}
/// <remarks>Should be used to attach events</remarks>
public override void OnActivate([NotNull] IScreen screen)
{
base.OnActivate(screen);
}
/// <remarks>Should be used to detach events</remarks>
public override void OnDeactivate([NotNull] 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 void OnViewReady([NotNull] IScreen screen,
object view)
{
base.OnViewReady(screen,
view);
}
public override void OnClose([NotNull] 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 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 void OnViewReady(IScreen screen,
object view)
{
base.OnViewReady(screen,
view);
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<DefaultDisplayNameRoutine>();
this.Container.PerRequest<MainController>();
}
protected override void OnStartup(object sender,
StartupEventArgs e)
{
Controller.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
}