Using Lokad Maybe Monads to simplify Desktop UI for CQRS solutions
Lokad Shared libraries had their own (immediate) implementation of Maybe Monad for a long time. I've just wanted to share how these monads work nicely with complex UI operations. This works out due to the pipeline syntax that has evolved in Lokad Shared Libs over the years.
Let's say that you have a read-only data grid (technically this is a table, not a grid any longer), populated from a CQRS read-model hosted somewhere in the cloud.
We would want to code a button that:
- selects a single record from the grid (or shows a message telling user to select only one);
- checks some precondition (or shows user message telling why he can't use this record);
- confirms with the user that he really wants to perform the operation;
- gets user impersonation id (showing UI if needed and quitting if he declines);
- composes a command if all is successful;
- sends the commands to Lokad.CQRS message bus running in Azure fabric (either dev or cloud);
- schedules UI refresh in a few seconds (refresh should come back in the UI thread).
Obviously, if user declines at some step within this chain or precondition fails - following items should not be executed.
Here's the actual code:
_solutions
.SelectSingle<UserSolutionView>()
.Combine(v => Check(v, x => x.State == "Ready", "Solution should be 'Ready'."))
.Combine(v => Confirm(v, "Do you really want to delete '{0}'?", v.Name))
.Combine(v => GetAdminId().Convert(id => new DeleteSolution(v.SolutionId, id)))
.Apply(cmd => _bus.Send(cmd))
.Apply(cmd => Schedule(RefreshSolutions, 3.Seconds()));
For the sake of completeness, here are some other operators that play in this chain (and are reused in the other chains):
// note, that this schedule function is simplified. It does not handle exceptions.
// make sure to follow TPL guidelines in your code.
void Schedule(Action action, TimeSpan span)
{
Task.Factory
.StartNew(() => lifetime.Token.WaitHandle.WaitOne(span))
.ContinueWith(t => action(), lifetime.Token,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.FromCurrentSynchronizationContext());
}
Maybe<TVal> Confirm<TVal>(TVal value, string message, params object [] args)
{
if (MessageBox.Show(this, string.Format(message, args),
"Confirm",
MessageBoxButtons.OKCancel,
MessageBoxIcon.Question) == DialogResult.OK)
return value;
return Maybe<TVal>.Empty;
}
// get some identifier cached in the monad
// prompt user for the identifier if it is not cached
// return nothing if user declines
public Maybe<long> GetAdminUserId()
{
if (UserId.HasValue) return UserId;
using (var form = new IdentityView(_query))
{
if (form.ShowDialog(this) == DialogResult.OK)
{
UserId = form.UserId;
}
}
return UserId;
}
// this is an extension method
public static Maybe<TView> SelectSingle<TView>(this DataGridView view)
where TView : PublishedView
{
if (view.MultiSelect == true)
{
throw Errors.InvalidOperation("Grid {0} should have multi-select == false", view.Name);
}
if (view.SelectedRows.Count == 0)
{
MessageBox.Show("You need to select a record first");
return Maybe<TView>.Empty;
}
return (TView) (view.SelectedRows[0].DataBoundItem);
}
Personally I like this pipelining syntax of Maybe Monad, since it simplifies writing multi-action UI sequences in C# for CQRS (and reduces chance of doing something wrong).
What do you think?
Thursday, September 16, 2010 at 2:25
Reader Comments (3)
I prefer a shortened syntax... wouldn't my Maybe project
well it says a hrefs are allowed but it appears it did not work... maybe.codeplex.com
Brandon, you didn't close the href - that's why it failed))
As for the maybes - nice start. I'd be interested to see how the project evolves to complex pipeline scenarios.