In my last post we had a look how I like to implement BusinessObjects
and a simple editor. In this post I will focus on one of the main areas for writing custom code: Controllers
and Actions
.
Controllers
As you know there are normally 2 types of Controllers. ViewControllers
and WindowControllers
. Thats very basic and not that accurate for a real application. Most of the time you deal with different types of things in an application. BusinessLogic
, view, state and so on.
Most of the applications i saw did a really bad job when it's about separating all this concerns. So let's have a look at ViewControllers
first.
First of all: delete those stupid designer files. They will haunt you later on.
ViewControllers
If you implement business logic most of the controllers will fit for one Type of BusinessObject
. This is especially true for controllers with Actions
.
Normally i like to call them BusinessObjectViewController
, this is a good descriptive name for them (if they act for List and DetailViews
and a single BusinessObject
).
So let's have a look:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DevExpress.ExpressApp;
namespace Scissors.ExpressApp
{
public abstract class BusinessObjectViewController<TObjectType> : BusinessObjectViewController<ObjectView, TObjectType>
where TObjectType : class
{
}
public abstract class BusinessObjectViewController<TView, TObjectType> : ViewController<TView>
where TObjectType : class
where TView : ObjectView
{
public event EventHandler<CurrentObjectChangingEventArgs<TObjectType>> CurrentObjectChanging;
public event EventHandler<CurrentObjectChangedEventArgs<TObjectType>> CurrentObjectChanged;
protected BusinessObjectViewController()
=> TargetObjectType = typeof(TObjectType);
protected override void OnActivated()
{
base.OnActivated();
UnsubscribeFromViewEvents();
SubscribeToViewEvents();
}
protected override void OnDeactivated()
{
UnsubscribeFromViewEvents();
base.OnDeactivated();
}
private void SubscribeToViewEvents()
{
if(View != null)
{
View.QueryCanChangeCurrentObject += View_QueryCanChangeCurrentObject;
View.CurrentObjectChanged += View_CurrentObjectChanged;
}
}
private void UnsubscribeFromViewEvents()
{
if(View != null)
{
View.QueryCanChangeCurrentObject -= View_QueryCanChangeCurrentObject;
View.CurrentObjectChanged -= View_CurrentObjectChanged;
}
}
void View_QueryCanChangeCurrentObject(object sender, CancelEventArgs e)
{
var args = new CurrentObjectChangingEventArgs<TObjectType>(e.Cancel, CurrentObject);
OnCurrentObjectChanging((TView)sender, args);
e.Cancel = args.Cancel;
}
protected virtual void OnCurrentObjectChanging(TView view, CurrentObjectChangingEventArgs<TObjectType> e)
=> CurrentObjectChanging?.Invoke(this, e);
void View_CurrentObjectChanged(object sender, EventArgs e)
=> OnCurrentObjectChanged((TView)sender, new CurrentObjectChangedEventArgs<TObjectType>(CurrentObject));
protected virtual void OnCurrentObjectChanged(TView view, CurrentObjectChangedEventArgs<TObjectType> args)
=> CurrentObjectChanged?.Invoke(this, args);
public TObjectType CurrentObject
=> View?.CurrentObject as TObjectType;
public IEnumerable<TObjectType> SelectedObjects
{
get
{
foreach(var item in View?.SelectedObjects?.OfType<TObjectType>())
{
yield return item;
}
}
}
}
public class CurrentObjectChangedEventArgs<TObjectType> : EventArgs
where TObjectType : class
{
public readonly TObjectType CurrentObject;
public CurrentObjectChangedEventArgs(TObjectType obj)
=> CurrentObject = obj;
}
public class CurrentObjectChangingEventArgs<TObjectType> : EventArgs
where TObjectType : class
{
public readonly TObjectType CurrentObject;
public bool Cancel { get; set; }
public CurrentObjectChangingEventArgs(TObjectType obj) : this(false, obj)
{
}
public CurrentObjectChangingEventArgs(bool cancel, TObjectType obj)
{
Cancel = cancel;
CurrentObject = obj;
}
}
}
As you can see it's a helper base class designed to work with an ObjectView
(for example ListViews
or DetailViews
) and a specific BusinessObject
. There are 2 additional methods you can overwrite: OnCurrentObjectChanging
and OnCurrentObjectChanged
. These are designed to subscribe and unsubscribe to events from the targeted BusinessObject
.
Also all the casting of the current and selected objects are handled.
The both events CurrentObjectChanging
and CurrentObjectChanged
are for convenient use from other controllers. Also the additional BusinessObjectViewController<TObjectType>
class is to avoid duplication if we don't care about the View
at all.
There are of course 2 base classes we can provide:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DevExpress.ExpressApp;
namespace Scissors.ExpressApp
{
public class BusinessObjectDetailViewController<TObjectType> : BusinessObjectViewController<DetailView, TObjectType>
where TObjectType : class
{
}
public class BusinessObjectListViewController<TObjectType> : BusinessObjectViewController<ListView, TObjectType>
where TObjectType : class
{
}
}
Note another important pattern in the BusinessObjectViewController
class. We always unsubscribe from events before we subscribe to them (in the OnActivated
method), and we unsubscribe before the base call in the OnDeactivated
. This will avoid duplicated subscriptions, as well as helping with managing the correct lifetime of events and avoid memory leaks later on.
For Controllers
in general, try to override the OnActivated
and OnDeactivated
and don't use the event approach. It's a lot saver to do. If you still use the designer.cs
approach, stop it. One merge conflict later and your stuff stops working, and you got no clue why.
Another thing is: You open a Controller file and see in the constructor whats going on. What BusinessObject
are you dealing with, what ViewId
and so on. No more digging in the designer
or the designer.cs
file to look for errors.
Actions
Let's talk about Actions
.
Actions are the main interaction (and abstraction) point of user interface in XAF. These are placed usually in the toolbar. So let's talk about the declaration.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DevExpress.ExpressApp.Actions;
using DevExpress.ExpressApp.Templates;
using DevExpress.Persistent.Base;
using Scissors.ExpressApp;
using Scissors.FeatureCenter.Modules.LabelEditorDemos.BusinessObjects;
#pragma warning disable IDE0021
namespace Scissors.FeatureCenter.Modules.LabelEditorDemos.Controllers
{
public class LabelDemoModelObjectViewController : BusinessObjectViewController<LabelDemoModel>
{
public SimpleAction LoremIpsumSimpleAction { get; }
public LabelDemoModelObjectViewController()
{
LoremIpsumSimpleAction = new SimpleAction(this, $"{GetType().FullName}.{nameof(LoremIpsumSimpleAction)}", PredefinedCategory.Edit)
{
Caption = "Insert Lorem Ipsum",
ImageName = "BO_Skull",
PaintStyle = ActionItemPaintStyle.Image,
ToolTip = "Inserts the famous lorem ipsum text into the selected demo object",
SelectionDependencyType = SelectionDependencyType.RequireSingleObject,
};
LoremIpsumSimpleAction.Execute += (s, e) =>
{
var txt = LoremIpsum(10, 10, 10, 10, 5);
CurrentObject.Text = txt;
};
}
static string LoremIpsum(
int minWords,
int maxWords,
int minSentences,
int maxSentences,
int numParagraphs)
{
var words = new[]
{
"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer",
"adipiscing", "elit", "sed", "diam", "nonummy", "nibh", "euismod",
"tincidunt", "ut", "laoreet", "dolore", "magna", "aliquam", "erat"
};
var rand = new Random();
var numSentences = rand.Next(maxSentences - minSentences) + minSentences + 1;
var numWords = rand.Next(maxWords - minWords) + minWords + 1;
var result = new StringBuilder();
for(var p = 0; p < numParagraphs; p++)
{
for(var s = 0; s < numSentences; s++)
{
for(var w = 0; w < numWords; w++)
{
if(w > 0)
{
result.Append(" ");
}
if(rand.Next(0, 100) % 2 == 0)
{
result.Append("<b>");
result.Append(words[rand.Next(words.Length)]);
result.Append("</b>");
}
else
{
result.Append(words[rand.Next(words.Length)]);
}
}
result.Append(". ");
}
result.Append(Environment.NewLine);
}
return result.ToString();
}
}
}
So let's have a look:
- First of all we implement a public property with a getter only for the Action which is good. You should always do this.
- The action has a full Id path. (namespace.controller.action) that is very handy if you really got a lot actions in your application.
- We defined the category right (this EDIT's the business object)
- We have a caption, tooltip and an image
- We defined the
PaintStyle
(if you got a lot of actions, you really want only a few actions to have text, especially with low resolutions).
Thats all fine so far, but there is one gotcha here: coupling of business logic to an controller. Thats bad. We have no easy way to test stuff inside an controller. But thats another best practice i get into A LOT in future blog posts.
As for the controller naming: LabelDemoModel
-ObjectViewController
. Name of the BusinessObject
plus ObjectViewController
. So it's clear when reading the name what is going on in this controller.
Let's see it in action:
Nice!
As you can see in the Execute
handler of the action we don't need to cast anymore, are typesafe and it's easy to use.
Note: There was a bug in the last post: You have to specify the
AutoSizeMode
of theLabelControl
toLabelAutoSizeMode.None
for correct wordwrap inside of aLayoutControl
using System;
using System.Linq;
using DevExpress.ExpressApp.Model;
using DevExpress.ExpressApp.Win.Editors;
using DevExpress.Utils;
using DevExpress.XtraEditors;
namespace Scissors.ExpressApp.LabelEditor.Win.Editors
{
public class LabelStringPropertyEditor : WinPropertyEditor
{
public LabelStringPropertyEditor(Type objectType, IModelMemberViewItem model)
: base(objectType, model)
=> ControlBindingProperty = nameof(Control.Text);
protected override object CreateControlCore()
{
var control = new LabelControl
{
AllowHtmlString = true,
AutoSizeMode = LabelAutoSizeMode.None, //THIS WAS MISSING
};
control.Appearance.TextOptions.WordWrap = WordWrap.Wrap;
return control;
}
public new LabelControl Control => (LabelControl)base.Control;
}
}
But wait, there is one step I was missing. Registering the controller!
using System;
using System.Linq;
using Scissors.FeatureCenter.Modules.LabelEditorDemos.Controllers;
namespace Scissors.FeatureCenter.Modules.LabelEditorDemos
{
public static class LabelEditorDemosControllers
{
public static readonly Type[] Types = new[]
{
typeof(LabelDemoModelObjectViewController)
};
}
}
Like the BusinessObjects
we define a separate class for the controller types. And then we need to register them.
using System;
using System.Collections.Generic;
using Scissors.ExpressApp;
using Scissors.FeatureCenter.Modules.LabelEditorDemos.BusinessObjects;
namespace Scissors.FeatureCenter.Modules.LabelEditorDemos
{
public sealed class LabelEditorDemosFeatureCenterModule : ScissorsBaseModule
{
protected override IEnumerable<Type> GetDeclaredExportedTypes()
=> LabelEditorDemosBusinessObjects.Types;
protected override IEnumerable<Type> GetDeclaredControllerTypes()
=> LabelEditorDemosControllers.Types;
}
}
Comments
Alex Miller 10 Mar 2018 19:48
Hi Manuel,
Very well thought. A lot of small details that will really help structure my app and avoid unforeseen problems! Thanks again for this awesome post. A quick question, how do you structure your controller when for example you want to share an action between two BO types that don't share the same base class (other than baseobject)? I use a lot of interfaces like IThumbnail and my ViewController's is set like this
TargetObjectType = typeof(IThumbnail);
Alex
Thank you
Your comment will appear in a few minutes.
Manuel Grundner 10 Mar 2018 21:06
Hi Alex! Thanks for your Feedback. Short: avoid It. I'll Cover this in the next Post!
Thanks for your Support :)
Thank you
Your comment will appear in a few minutes.
Kirsten Greed 11 Dec 2018 16:05
Hi Manuel, Thanks for an excellent post with heaps to learn from. I found that I need to set the startup object in the Cli project ( right click->Properties-> Application ) to my version of CliProgram , so as to avoid a "Compile with /main to specify the type that contains the entry point" error.
I was unable to find the shared project template in VS 15.9.3 So I cloned the project from your solution. I asked about that at https://stackoverflow.com/questions/53690103/missing-shared-visual-studio-project-template:Dk8xUijReaQQjjjx7dnFOp5LT0M
Also when I upgraded my clone of your solution I had an issue with the Nuget upgrade. https://www.devexpress.com/Support/Center/Question/Details/T698381/nu1102-unable-to-find-package-devexpress-xtrareports-with-version-18-2-3:gIqVCSBS4_DBo4cLvjKUhN4-_SQ
https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-packaging-dot-net:dCQY-JwyAlzTs12-bFNkpepafjk was also helpful to create the package
Kirsten
Thank you
Your comment will appear in a few minutes.
Thank you
Your comment will appear in a few minutes.