Search This Blog

Thursday, October 25, 2012

"Ideal" development workflow for adding a custom functionality in XAF

In this "short" post I'd like to demonstrate the subject by example of implementing the following suggestion: SystemModules.Link - Exclude objects that are already linked to an object from the Link ListView. Good for our learning purposes, this feature request is representative and small enough at the same time. Technically, we will improve the standard LinkUnlinkController class behavior. Let's start doing real coding!

Implementation/Refactoring

If you look at the current 11.2 code of the controller, you will notice that it is quite difficult to provide your own link View for it:

private void linkAction_OnCustomizePopupWindow(Object sender, CustomizePopupWindowParamsEventArgs args) {
linkListView = (ListView)CreateLinkView();
args.View = linkListView;
args.DialogController = Application.CreateController<LinkDialogController>();
((LinkDialogController)args.DialogController).Initialize(lookUpEditorHelper,
lookUpEditorHelper.CanFilterDataSource(linkListView.CollectionSource, MasterObject));
}

Note that according to this code the link View must be always a ListView. Let's fix this by adding more flexibility as follows:

private void linkAction_OnCustomizePopupWindow(Object sender, CustomizePopupWindowParamsEventArgs args) {
View linkView = CreateLinkView();
args.View = linkView;
linkListView = linkView as ListView;
if (linkListView != null && lookUpEditorHelper != null) {
LinkDialogController dialogController = Application.CreateController<LinkDialogController>();
dialogController.Initialize(lookUpEditorHelper, lookUpEditorHelper.CanFilterDataSource(linkListView.CollectionSource, MasterObject));
args.DialogController = dialogController;
}
}

As you see, I removed the direct cast to ListView and also slightly refactored the rest code. Apparently, now the linkListView variable must be checked on null before using.

If you look at the current CreateLinkView method code, you will notice that it creates the link View of the ListView type only. Since we want to provide the capability to create the link View of an arbitrary View type (e.g., check out this ticket for a real business scenario), we can refactor the method by introducing the event and also moving the default ListView creation code into a separate method:

protected virtual View CreateLinkView() {
CustomCreateLinkViewEventArgs customArgs = new CustomCreateLinkViewEventArgs(View);
OnCustomCreateLinkView(customArgs);
if (!customArgs.Handled || customArgs.LinkView == null)
return CreateLinkViewCore();
return customArgs.LinkView;
}
protected virtual void OnCustomCreateLinkView(CustomCreateLinkViewEventArgs customArgs) {
if (CustomCreateLinkView != null)
CustomCreateLinkView(this, customArgs);
}

The new event and its arguments declarations will look as follows:

public event EventHandler<CustomCreateLinkViewEventArgs> CustomCreateLinkView;

public class CustomCreateLinkViewEventArgs : HandledEventArgs {
private ListView sourceViewCore;
public CustomCreateLinkViewEventArgs(ListView sourceView) {
sourceViewCore = sourceView;
}
public ListView SourceView { get { return sourceViewCore; } }
public View LinkView { get; set; }
}


To complete support of custom link Views, we need to slightly refactor the Link method code, because it currently takes selected objects from the default ListView only:

protected virtual void Link(PopupWindowShowActionExecuteEventArgs args) {
LinkObjects(args.PopupWindow.View.SelectedObjects);
}

My refactored code is shown below:

protected virtual void Link(PopupWindowShowActionExecuteEventArgs args) {
QueryLinkObjectsEventArgs customArgs = new QueryLinkObjectsEventArgs(args.PopupWindow);
OnQueryLinkObjects(customArgs);
IList linkObjects = customArgs.LinkObjects;
if (!customArgs.Handled && linkListView != null)
linkObjects = linkListView.SelectedObjects;
LinkObjects(linkObjects);
}
protected virtual void OnQueryLinkObjects(QueryLinkObjectsEventArgs customArgs) {
if (QueryLinkObjects != null)
QueryLinkObjects(this, customArgs);
}

Here the QueryLinkObjects event and its arguments look as follows:

public event EventHandler<QueryLinkObjectsEventArgs> QueryLinkObjects;
public class QueryLinkObjectsEventArgs : HandledEventArgs {
private Window linkWindowCore;
public QueryLinkObjectsEventArgs(Window linkWindow) {
linkWindowCore = linkWindow;
}
public Window LinkWindow {
get { return linkWindowCore; }
}
public IList LinkObjects { get; set; }
}

So, a guy who wants to implement a custom link View will have to handle these two events, provide their respective LinkView and LinkObjects parameters and do not forget to set the important Handled parameter to True.

We are now done with the first part and are ready to implement the main functionality - excluding already linked objects from the link View. Apparently, we need to modify the newly introduced CreateLinkViewCore method for that purpose. Since we are creating a ListView in this method, we need to add a criterion to its CollectionSource, as described in this help article from our docs:

if(MasterObject != null) {
CriteriaOperator associatedCollectionCriteria =
ObjectSpace.GetAssociatedCollectionCriteria(MasterObject, ((PropertyCollectionSource)View.CollectionSource).MemberInfo);
result.CollectionSource.Criteria[CriteriaKeyForLinkView] = new NotOperator(associatedCollectionCriteria);
}

As you see, I just took association criteria and then inverted it.

It seems that we are done with our main implementation and now it is time to check whether this code ever works. The easiest way to do this is to run our MainDemo application and test the improved Link functionality with Contact and its Tasks list:

Linkunlinktest

Thanks God, my code worked like a sharm in both Windows Forms and ASP.NET applications. Some of you may think that we are now fully done, but in fact we are only in the middle of our path... Why? The answer is simple - we must ensure that the code we just wrote will work fine in future versions of our product, preferably without any input from God.

Unit Testing

The best way to ensure this is to cover the implemented functionality with unit and functional tests. I strongly believe that it is simply damn wrong and plain stupid not to do this (no mercy) and as a result manually test this functionality each and every release in the future. Writing tests will also make you a better developer.

Fortunately for me (and for you too!), the developers of our Core team already had some unit tests for LinkUnlinkController in the LinkUnlinkControllerTests class and my task is to just add a few test methods to cover my code. Practically, that means that I do not need to create a required testing infrustructure from scratch and can just reuse existing test objects and probably grab some example code blocks from other tests. Another good thing is that I can make use of existing base test classes and mocks like BaseXafTest, TestApplication, etc. that provide useful methods and allow me to write platform-independent tests. Let's demonstrate this by an example.

First, I will extend the LinkUnlinkController descendant used only for tests with a couple of overridden methods to provide counters for my events - I want to ensure that they actually fire when needed and their arguments are taken into account:

public class LinkUnlinkControllerForTests : LinkUnlinkController {
public int CustomCreateLinkViewEventCounter = 0;
public int QueryLinkObjectsEventCounter = 0;
public LinkUnlinkControllerForTests() : base() { }
protected override void OnCustomCreateLinkView(CustomCreateLinkViewEventArgs customArgs) {
base.OnCustomCreateLinkView(customArgs);
CustomCreateLinkViewEventCounter++;
}
protected override void OnQueryLinkObjects(QueryLinkObjectsEventArgs customArgs) {
base.OnQueryLinkObjects(customArgs);
QueryLinkObjectsEventCounter++;
}
}

This is a very common practice, especially when we need to hook up into some protected members or disable/mock certain functionality for tests only. To give you one more example, the TestApplication class I mentioned earlier is a descendant of XafApplication that overrides its methods for tests, e.g. provides platform-agnostic TestListEditor and TestLayoutManager instead of real platform-dependent classes.

Now we are ready to write our first test method for the functionality implemented earlier:

[Test]
public void TestCustomCreateLinkViewEvent() {
RegisterTypesForModel(typeof(Incident), typeof(Message));
CollectionSourceBase linkedMessagesDS = CreateCollectionSource(incident, "Messages");
TestApplication application = new TestApplication(this, modelApplication);
LinkUnlinkControllerForTests controller = new LinkUnlinkControllerForTests();
controller.Application = application;
ListView sourceListView = application.CreateListView(GetListView<Message>(), linkedMessagesDS, false);
sourceListView.CreateControls();
controller.SetView(sourceListView);
Assert.AreEqual(controller.LinkAction.GetPopupWindowParams().View.GetType(), typeof(ListView));
Assert.AreEqual(1, controller.CustomCreateLinkViewEventCounter);
bool handled = false;
EventHandler<CustomCreateLinkViewEventArgs> customCreateLinkViewEventHandler = delegate(object sender, CustomCreateLinkViewEventArgs args) {
Assert.AreEqual(args.SourceView, sourceListView);
args.LinkView = new DashboardView(ObjectSpace, application, false);
args.Handled = handled;
};
controller.CustomCreateLinkView += customCreateLinkViewEventHandler;
Assert.AreEqual(controller.LinkAction.GetPopupWindowParams().View.GetType(), typeof(ListView));
Assert.AreEqual(2, controller.CustomCreateLinkViewEventCounter);
handled = true;
Assert.AreEqual(controller.LinkAction.GetPopupWindowParams().View.GetType(), typeof(DashboardView));
Assert.AreEqual(3, controller.CustomCreateLinkViewEventCounter);
controller.CustomCreateLinkView -= customCreateLinkViewEventHandler;
}

It's a bit too long, but I need to test all possible combinations.

The next two test methods will be a way more complex and longer:

[Test]
public void TestQueryLinkObjectsEventAndExcludeLinkedObjectsNoServerMode() {
TestQueryLinkObjectsEventAndExcludeLinkedObjectsCore(false);
}
[Test]
public void TestQueryLinkObjectsEventAndExcludeLinkedObjectsServerMode() {
TestQueryLinkObjectsEventAndExcludeLinkedObjectsCore(true);
}

private void TestQueryLinkObjectsEventAndExcludeLinkedObjectsCore(bool useServerMode) {
RegisterTypesForModel(typeof(Incident), typeof(Message));
CollectionSourceBase linkedMessagesDS = CreateCollectionSource(incident, "Messages");
TestApplication application = new TestApplication(this, modelApplication);
application.Model.Views.DefaultListEditor = typeof(TestListEditor);
application.Model.Options.UseServerMode = useServerMode;
LinkUnlinkControllerForTests controller = new LinkUnlinkControllerForTests();
controller.Application = application;
ListView sourceListView = application.CreateListView(GetListView<Message>(), linkedMessagesDS, false);
sourceListView.CreateControls();
controller.SetView(sourceListView);
CustomizePopupWindowParamsEventArgs windowParams = controller.LinkAction.GetPopupWindowParams();
Assert.AreEqual(1, linkedMessagesDS.GetCount());
Assert.AreEqual(windowParams.View.GetType(), typeof(ListView));
Assert.AreEqual(0, controller.QueryLinkObjectsEventCounter);
bool handled = false;
Window linkWindow = new Window(application, "", null, false, true);
EventHandler<QueryLinkObjectsEventArgs> queryLinkObjectsEventHandler = delegate(object sender, QueryLinkObjectsEventArgs args) {
args.LinkObjects = new object[0];
args.Handled = handled;
Assert.AreEqual(linkWindow, args.LinkWindow);
Assert.AreEqual(windowParams.View, args.LinkWindow.View);
};
controller.QueryLinkObjects += queryLinkObjectsEventHandler;
ListView linkListView = ((ListView)windowParams.View);
CollectionSourceBase messagesToLinkDS = linkListView.CollectionSource;
Assert.IsTrue(messagesToLinkDS.Criteria.ContainsKey(LinkUnlinkController.CriteriaKeyForLinkView));
Assert.AreEqual(0, messagesToLinkDS.GetCount());
Message messageToLink = new Message(ObjectSpace.Session);
ObjectSpace.CommitChanges();
messagesToLinkDS.Reload();
Assert.AreEqual(1, messagesToLinkDS.GetCount());
linkWindow.SetView(linkListView);
linkListView.CreateControls();
linkListView.Editor.FocusedObject = messageToLink;
controller.LinkAction.DoExecute(linkWindow);
Assert.AreEqual(1, controller.QueryLinkObjectsEventCounter);
ObjectSpace.CommitChanges();
incident.Reload();
Assert.AreEqual(2, incident.Messages.Count);
handled = true;
messageToLink = new Message(ObjectSpace.Session);
ObjectSpace.CommitChanges();
linkListView.Editor.FocusedObject = messageToLink;
controller.LinkAction.DoExecute(linkWindow);
Assert.AreEqual(2, controller.QueryLinkObjectsEventCounter);
ObjectSpace.CommitChanges();
incident.Reload();
Assert.AreEqual(2, incident.Messages.Count);
controller.QueryLinkObjects -= queryLinkObjectsEventHandler;
}

However, these methods fully test the entire link functionality from the beginning to end. It is also done under both server-side and client binding modes.

Did I mention that writing good unit tests once and for all will help you avoid problems and PITA in the future?

Functional Testing

Finally, to ensure that everything will operate as expected under real circumstances, I will add functional tests. As you probably know, these kind of tests is powered by our EasyTest functional testing framework. With EasyTest I can ensure the correctness of both Windows Forms and ASP.NET applications using a single test script:

#Aplication AppStudioTestWeb
#Application AppStudioTestWin

*Action Navigation(Incident)
*ProcessRecord
Описание = Test Incident

*Action Versions

*Action Versions.Link
*CheckTable
Columns = Name
Row = 1.0
Row = 2.0
*ProcessRecord
Name = 2.0

*CheckTable Versions
Columns = Name
Row = 2.0

*Action Versions.Link
*CheckTable
Columns = Name
Row = 1.0
*Action Cancel

*SelectRecords Versions
Columns = Name
Row = 2.0
*Action Versions.Unlink

*HandleDialog
Respond = Yes

!ProcessRecord Versions
Name = 2.0

That is it. Now I am sure that the code I added will not be broken one day. Or at least I will know if that happens from failed tests.

I hope you find this information interesting as the process I described above will help you ensure a better quality of your XAF applications. I also want to explicitly mention that this process is the same in our team when developing features. The only thing I did not mention is that we also do a manual testing of some scenarios before releasing a new product version.

Forgot to say, that the feature I described here will be available in XAF 12.1. I also hope you also liked this small addition to the framework.

See Also
Best practices of creating reusable XAF modules by example of a View Variants module extension
eXpressApp Framework > Task-Based Help > Testing

No comments:

Post a Comment