Creating a State Machine module for eXpand Framework–Part 2
Let me describe for a moment how we at DevExpress work. We build and sell software which means that we only sell and provide support for products that have been built and tested by us! However I am here as a framework evangelist and huge XAF fan. This makes it my duty to spread the word as much as I can and make XAF even bigger. To this end through collaboration within the XAF community, we have been building and supporting eXpand. This framework follows XAF to the letter and takes things even further. eXpand gets its inspiration from real life situations and bases itself on examples from DevExpress Support Center. eXpand is the first open source project based on the DevExpress eXpressApp Framework (XAF). More info is available at www.expandframework.com and our very existence relies on your efforts! Anyone is welcome to contribute and enjoy the rewards. It is not necessary to be a XAF guru, we can all manage to create a behavior taken from DevExpress code central. Let’s work together to enhance our beloved XAF!
Prerequisites
Part 1
In this post we are going to enhance the State Machine module UI. Remember that along with all the usual XAF goodies we can now use Xpand code base which gives us a lot more options. Our StateMachineTransitionPermission has 2 lookups, StateMachineName and StateMachine. Our goal is to populate both of these cascading lookups without creating a platform specific module.
StateMachineNames
Creating lookups is a common scenario for which Xpand provides a set of property editors and controllers. By contrast with other business frameworks XAF allows maximum flexibility. Therefore in most cases we are able to code in such a generic way that everything could live in separate frameworks such as eXpand. Now, in order to populate the StateMachine name I am going to derive a new controller from a specialized abstract controller which is Xpand.ExpressApp.SystemModule.PopulateController<T>. This controller uses the PredefinedValues attribute of the XAF model. When filling the attribute with a set of values separated by semicolons XAF will create a lookup with these values targeting each supported platform.
However, if at runtime we set the value of the PredefinedValues attribute this will be written at model’s lastlayer and it will make it dirty. We want to avoid this because we want to leave the lastlayer intact.. To cater for this need the populate controller uses a hack. First it stores the lastlayer in a variable then removes it from the model’s layers collection. As a result it is possible to modify the new lastlayer as shown in the Populate method and then return the clean old one to its place. Now the model has all the necessary information with a clean userdiffs layer and while XAF is creating a new view can get the PredefinedValues string from it and create the lookups.
public abstract class PopulateController<T> : ViewController<ObjectView> {
…
…
protected virtual void Populate(Func<IModelMember, string> collect) {
var name = PropertyName;
if (name != null) {
var model = ((ModelApplicationBase)Application.Model);
var lastLayer = model.LastLayer;
model.RemoveLayer(lastLayer);
PopulateCore(collect, name);
model.AddLayer(lastLayer);
}
}
private void PopulateCore(Func<IModelMember, string> collect, string propertyName) {
IModelMember modelMember = View.Model.ModelClass.AllMembers.FirstOrDefault(member => member.Name == propertyName);
if (modelMember != null) {
modelMember.PredefinedValues = collect.Invoke(modelMember);
}
}
…
…
}
Although this seems like a complicated explanation users need not be intimidated! The implementation of our controller that will populate all StateMachineNames is as simple as this,
public class StateMachinePopulateController : PopulateController<StateMachineTransitionPermission> {
protected override string GetPredefinedValues(IModelMember wrapper) {
IList<XpoStateMachine> xpoStateMachines = ObjectSpace.GetObjects<XpoStateMachine>(null);
return xpoStateMachines.Select(machine => machine.Name).AggregateWith(";");
}
protected override Expression<Func<StateMachineTransitionPermission, object>> GetPropertyName() {
return permission => permission.StateMachineName;
}
}
[ImmediatePostData]
public string StateMachineName { get; set; }
//IStringLookupPropertyEditor lives in Xpand.ExpressApp assembly
//Xpand.ExpressApp.Web.PropertyEditors.StringLookupPropertyEditor, Xpand.ExpressApp.Win.PropertyEditors.StringLookupPropertyEditor inherit from IStringLookupPropertyEditor
[PropertyEditor(typeof(IStringLookupPropertyEditor))]
[DataSourceProperty("StateCaptions")]
public string StateCaption { get; set; }
IList<string> _stateCaptions = new List<string>();
[Browsable(false)]
public IList<string> StateCaptions {get {return _stateCaptions;}}
If you look carefully at this code however you may notice that __stateCaptions count is always zero. Let me remind you here that the StateMachineTransitionPermission is a non persistent sessionless object. This means that the object is not handled by an ObjectSpace therefore a call like ObjectSpace.FindObjectSpaceByObject(this) will always return null. In addition the permission does not implement INotifyPropertyChanged so we need to synchronize the class just before the StateCaptions are requested. Below you can see a modified version of the StateMachinePopulateController,
public class StateMachinePopulateController : PopulateController<StateMachineTransitionPermission> {
protected override void OnViewControlsCreated() {
base.OnViewControlsCreated();
var stringLookupPropertyEditor = GetPropertyEditor(permission => permission.StateCaption) as IStringLookupPropertyEditor;
if (stringLookupPropertyEditor != null)
stringLookupPropertyEditor.ItemsCalculating += StringLookupPropertyEditorOnItemsCalculating;
}
void StringLookupPropertyEditorOnItemsCalculating(object sender, HandledEventArgs handledEventArgs) {
var propertyEditor = GetPropertyEditor(permission => permission.StateMachineName);
if (propertyEditor != null && View.IsControlCreated) {
var stateMachineTransitionPermission = ((StateMachineTransitionPermission)View.CurrentObject);
var readOnlyCollection = GetStateCaptions(propertyEditor);
stateMachineTransitionPermission.SyncStateCaptions(readOnlyCollection, propertyEditor.ControlValue as string);
}
}
ReadOnlyCollection<string> GetStateCaptions(PropertyEditor propertyEditor) {
var stateMachineName = propertyEditor.ControlValue as string;
return ObjectSpace.GetObjects<XpoState>(state => state.StateMachine.Name == stateMachineName).Select(
state => state.Caption).ToList().AsReadOnly();
}
Finally we add the new SyncStateCaptions method and the full version of the permission will be,
[NonPersistent]
public class StateMachineTransitionPermission : PermissionBase {
…
…
[ImmediatePostData]
public string StateMachineName { get; set; }
[PropertyEditor(typeof(IStringLookupPropertyEditor))]
[DataSourceProperty("StateCaptions")]
public string StateCaption { get; set; }
IList<string> _stateCaptions = new List<string>();
[Browsable(false)]
public IList<string> StateCaptions {get {return _stateCaptions;}}
public void SyncStateCaptions(IList<string> stateCaptions, string machineName) {
StateMachineName = machineName;
_stateCaptions = stateCaptions;
}
}
To support platform independent cascading lookups we wrote only about 10 lines of code! This is proof of how much XAF architecture cuts down on development costs. The module can be downloaded from the Xpand download page and we are happy to hear your feedback. Remember that your questions are the best candidates for future posts!