XAF best practices 2017-04
12 March 2018 by Manuel Grundner
In my last post i covered a small portion of business logic, containing controllers and actions. Today I'll cover 2 things: Avoiding string magic with View-ID's and how to build criterias.
View-ID's
A lot of code i see when reviewing others XAF-Source code is dealing with View-ID's.
XAF generates 3 View's for every BusinessObject: a
DetailView
, a ListView
and a LookupListView
. They are structured the following way: NameOfTheBusinessObject_TypeOfView
.
Those view id's are often used in controllers, and sometimes stored in the database for more dynamic applications.
I didn't know about this handy helper class provided by the XAF-Team: The
DevExpress.ExpressApp.Model.NodeGenerators.ModelNodeIdHelper
!
It's a simple helper class with 4 very helpful methods:
GetDetailViewId
, GetListViewId
, GetLookupListViewId
and GetNestedListViewId
!
Let's have a look:
var detailViewId = ModelNodeIdHelper.GetDetailViewId(typeof(LabelDemoModel)); //LabelDemoModel_DetailView
var listViewId = ModelNodeIdHelper.GetListViewId(typeof(LabelDemoModel)); //LabelDemoModel_ListView
var lookupListViewId = ModelNodeIdHelper.GetLookupListViewId(typeof(LabelDemoModel)); //LabelDemoModel_LookupListView
The last one
GetNestedListViewId
provides a LookupLiewView
of a nested ListView
. For example if you got an aggregate root Person
with an one to many relationship Contacts
(for example phone, email, ect.).
The usage will look something like this:
var nestedListViewId = ModelNodeIdHelper.GetNestedListViewId(typeof(Person), nameof(Person.Contacts)); //Person_Contacts_ListView
In this case XAF will generate a separate nested
ListView
of the Contacts
type excluding the reference on Person
. This is logical, cause in a nested ListView the Person
field doesn't make sense.
Another thing i like to provide is a separate class in the
Contracts
assembly to provide easy access to all ViewId's that are used in code.using System;
using System.Linq;
using DevExpress.ExpressApp.Model.NodeGenerators;
namespace Scissors.FeatureCenter.Modules.LabelEditorDemos.Contracts
{
public static class ViewIds
{
public static class LabelDemoModel
{
public static readonly string DetailView = ModelNodeIdHelper.GetDetailViewId(typeof(BusinessObjects.LabelDemoModel));
public static readonly string ListView = ModelNodeIdHelper.GetListViewId(typeof(BusinessObjects.LabelDemoModel));
public static readonly string LookupListView = ModelNodeIdHelper.GetDetailViewId(typeof(BusinessObjects.LabelDemoModel));
}
}
}
So you can write a controller like this:
public LabelDemoModelObjectViewController()
{
TargetViewId = ViewIds.LabelDemoModel.DetailView;
}
Criterias
Next we look how we can avoid strings when building criterias. This will help a lot if you need to refactor code later and don't want to break every criteria you've written so far. There are 2 possible options. The first one is using the
FieldsClass
that is now integrated into CodeRush
since 17.1.5
Another way is to use the power of Expressions. Thats the stuff that powers LINQ.
Lets have a look:
using System;
using System.Linq.Expressions;
using System.Text;
namespace Scissors.Utils
{
public static class ExpressionHelper
{
public static MemberExpression GetMemberExpression(Expression expression)
{
if(expression is MemberExpression)
{
return (MemberExpression)expression;
}
if(expression is LambdaExpression)
{
var lambdaExpression = expression as LambdaExpression;
if(lambdaExpression.Body is MemberExpression)
{
return (MemberExpression)lambdaExpression.Body;
}
if(lambdaExpression.Body is UnaryExpression)
{
return ((MemberExpression)((UnaryExpression)lambdaExpression.Body).Operand);
}
}
return null;
}
public static string GetPropertyPath(Expression expr)
{
var path = new StringBuilder();
var memberExpression = GetMemberExpression(expr);
do
{
path.Insert(0, $".{memberExpression.Member.Name}");
if(memberExpression.Expression is UnaryExpression ue)
{
memberExpression = GetMemberExpression(ue.Operand);
}
else
{
memberExpression = GetMemberExpression(memberExpression.Expression);
}
}
while(memberExpression != null);
path.Remove(0, 1);
return path.ToString();
}
}
}
Now lets have a look what that does:
using System;
using System.Linq;
using System.Linq.Expressions;
using Shouldly;
using Xunit;
namespace Scissors.Utils.Tests
{
public class ExpressionHelperTests
{
class TargetClass
{
public TargetClass A { get; set; }
public TargetClass B { get; set; }
public TargetClass C { get; set; }
}
string PropertyName(Expression> expression)
=> ExpressionHelper.GetPropertyPath(expression);
[Fact]
public void SimplePathA()
=> PropertyName(m => m.A).ShouldBe("A");
[Fact]
public void SimplePathB()
=> PropertyName(m => m.B).ShouldBe("B");
[Fact]
public void SimplePathC()
=> PropertyName(m => m.C).ShouldBe("C");
[Fact]
public void ComplexPath1()
=> PropertyName(m => m.A.A.A.B.C.A).ShouldBe("A.A.A.B.C.A");
[Fact]
public void ComplexPath2()
=> PropertyName(m => m.C.A.B).ShouldBe("C.A.B");
}
}
Awesome! That helps us to write a simple helper class for XPO:
using System;
using System.Linq;
using System.Linq.Expressions;
using DevExpress.Data.Filtering;
using DevExpress.Xpo;
using Scissors.Utils;
namespace Scissors.Xpo
{
public class ExpressionHelper<TObj>
{
public string Property(Expression> expr)
=> GetPropertyPath(expr);
public OperandProperty Operand(Expression> expr)
=> GetOperand(expr);
public OperandProperty TypeOperand(Expression> expr)
=> new OperandProperty($"{ExpressionHelper.GetPropertyPath(expr)}.{XPObjectType.ObjectTypePropertyName}.TypeName");
public BinaryOperator IsType(Expression> expr, Type t)
=> TypeOperand(expr) == t.FullName;
public static string GetPropertyPath(Expression> expr)
=> ExpressionHelper.GetPropertyPath(expr);
public static OperandProperty GetOperand(Expression> expr)
=> new OperandProperty(ExpressionHelper.GetPropertyPath(expr));
public static BinaryOperator GetObjectTypeOperator(Expression> expr, Type objectType)
=> new OperandProperty($"{ExpressionHelper.GetPropertyPath(expr)}.{XPObjectType.ObjectTypePropertyName}") == objectType.FullName;
public static BinaryOperator GetObjectTypeOperator()
=> new OperandProperty(XPObjectType.ObjectTypePropertyName) == typeof(TObj).FullName;
}
}
Cool stuff! How do we use it? Extend the
LabelDemoModel
class: [Persistent]
public class LabelDemoModel : ScissorsBaseObjectOid
{
public static readonly ExpressionHelper Field = new ExpressionHelper();
}
Now we can use it like this:
var criteria = LabelDemoModel.Field.Operand(m => m.Text) == "Test";
That is very handy, cause you now never need to update the
FieldsClass
. It will cost a little bit performance, but I think the advantages outweigh the disadvantages. And cause the DevExpress team implemented several operators you can write even more complex operators!var criteria = BugModel.Field.Operand(m => m.Done).Not() & BugModel.Field.Operand(m => m.User).IsNotNull() & BugModel.Field.Operand(m => m.States[StateModel.Field.Operand(s => s.Active).Count() > 0];
It's a little bit more verbose, but on the other hand it's easy to read, refactor and you get full intellisense! The other methods on the
ExpressionHelper
class are for dealing with the ObjectType
of an XPO class a lot. But most of the time you don't need them.
Another thing I like to do very often is add a class that collects the CriteriaOperators used in an module in the
Contracts
or Domain
assembly. So you can reuse the criterias:public static class LabelDemoModelCriterias
{
public static CriteriaOperator NotEmpty()
=> LabelDemoModel.Field.Operand(m => m.Text).IsNotNull()
& new FunctionOperator(FunctionOperatorType.IsNullOrEmpty, LabelDemoModel.Field.Operand(m => m.Text));
}
I hope this will help some people write more robust XAF applications. Tell me what you think!
No comments:
Post a Comment