Wednesday, February 25, 2009

XAF Usability: Limit SpinEdit values with XAF Rules

In this new XAF Usability post, we will show how you can limit max and min values dynamically when editing integer numeric properties with SpinEdit controls.

Say your project has a XAF entity called Customer with an integer property PaymentDay aimed to store the number of the day of the month that each Customer schedules payments to his providers. As days of month are known in advance, and limited in number, a range rule is defined at property level to set max and min values for such property.


Once written down, your source code might look like this:

public partial class Customer : BaseObject
{
[...]
private int _PaymentDay;
[DisplayName("Payment day")]
// Define a rule to set min and max value for the property
[RuleRange("Customer.PaymentDay.RR", "Save", 0, 31)]
public virtual int PaymentDay
{
get { return _PaymentDay; }
set { SetPropertyValue("PaymentDay", ref _PaymentDay, value); }
}
[...]
}

Ideally, XAF might set Max and Min properties according to the rule range when rendering that kind of property thru a SpinEditor. But the truth is that it won’t do it, so you will be the one who have to. You can do it by adding a new ViewController to Module.Win project of your Solutions. Such ViewController would target any DetailView in your application, checking for every IntegerPropertyEditor created. When a property editor of that kind is added to the DetailView, it will use Application ValidationModule’s RulesWithoutSerializer method to get the rule associated to the property corresponding to the PropertyEditor being created. If the type of Rule is RuleRange, then its Min and Max values are used to set IntegerPropertyEditor’s Min and Max properties, as shown in the following code:

public partial class CustomSpinRuleRange : ViewController
{
public CustomSpinRuleRange()
{
InitializeComponent();
RegisterActions(components);
// Target ViewController to any DetailView in your application
this.TargetViewType = ViewType.DetailView;
}

protected override void OnActivated()
{
// Suscribe LayoutManager’s ItemCreated event handler to be able to check for every item created in the DetailView
((DevExpress.ExpressApp.Win.Layout.WinLayoutManager)((View as DetailView).LayoutManager)).ItemCreated += new EventHandlerdevexpress.expressapp.win.layout.ItemCreatedEventArgs(CustomSpinRange_ItemCreated);
base.OnActivated();
}

void CustomSpinRange_ItemCreated(object sender, DevExpress.ExpressApp.Win.Layout.ItemCreatedEventArgs e)
{
// For every new item created in the DetailView, check if it’s an IntegerPropertyEditor
if (e.DetailViewItem != null && e.DetailViewItem is DevExpress.ExpressApp.Win.Editors.IntegerPropertyEditor)
{
DevExpress.ExpressApp.Win.Editors.IntegerPropertyEditor lIntegerPropertyEditor = (DevExpress.ExpressApp.Win.Editors.IntegerPropertyEditor)e.DetailViewItem;
if (lIntegerPropertyEditor != null)
{
//If a PropertyEditor is being created, check if has an associated RuleRangeAttribute
DevExpress.Persistent.Validation.RuleRangeAttribute lRuleRangeAttribute = ((DevExpress.ExpressApp.Editors.PropertyEditor)
(lIntegerPropertyEditor)).MemberInfo.FindAttribute();
if (lRuleRangeAttribute != null)
{
// If a RuleRangeAttribute is was found, get an instance of the Application’s ValidationModule
// to gain access to validation rules
ValidationModule lModule = (ValidationModule)Application.Modules.FindModule(typeof(ValidationModule));
if (lModule != null)
{
DevExpress.Persistent.Validation.IRule lIRule;
// Use ValidationModule’s RulesWithoutSerializer method to get the rule associated to
// the property corresponding to the PropertyEditor being created in a safely manner
if (lModule.RulesWithoutSerializer.TryGetValue(lRuleRangeAttribute.Name, out lIRule))
{
// Once the rule was gotten, set PropertyEditor’s Min and Max values to RuleRange’s ones
DevExpress.Persistent.Validation.RuleRange lRuleRange = lIRule as DevExpress.Persistent.Validation.RuleRange;
lIntegerPropertyEditor.Control.Properties.MinValue = Convert.ToInt32(lRuleRange.Properties.MinimumValue);
lIntegerPropertyEditor.Control.Properties.MaxValue = Convert.ToInt32(lRuleRange.Properties.MaximumValue);
}
}
}
}
}
}
}

When the application is run and users edit Payment day property, they are not allowed to spin value under 0 or above 31. On the other side, if they type a value out of the valid range, a validation exception will raise when Customer entity is saved as XAF checks rule “Customer.PaymentDay.RR” for “Save” context.

Monday, February 16, 2009

XAF Usability: Show Expand Button in Layout groups

Our second post on Usability in XAF will show you how to improve user experience while using complex layouts where Property Editors have been organized in Layout Groups.

When entities have a large number of properties or one or more of its properties require large Property Editors, layout design for such entities soon become uncomfortable to read, navigate and edit by users. XAF's provides two solutions for this situation: tabs and layout item groups.

While tabs allow developers to arrange a larger number of controls on a form, they have two main drawbacks. First, if more than one tab is used vertical space consumed is that of the taller tab. Second, they are somewhat old-fashion looking. You can overcome all this using layout item groups. But once at runtime, you should realize that XAF’s item groups have fixed vertical size and that they don’t provide any user interface to collapse nor expand them.


That’s because item groups’ ExpandButtonVisible property is set to true by default, so it’s hidden for every group you create. You can switch it to true by means of a ViewController targeted to ViewType.DetailView as shown in the following code:


public partial class CustomDetailViewGroupBehaviour : ViewController
{
public CustomDetailViewGroupBehaviour()
{
InitializeComponent();
RegisterActions(components);
// Target ViewController to any DetailView in your application
TargetViewType = ViewType.DetailView;
}

protected override void OnActivated()
{
base.OnActivated();
// Suscribe LayoutManager’s ItemCreated event handler to be able to check for every item created in the DetailView
((WinLayoutManager)((View as DetailView).LayoutManager)).ItemCreated += new EventHandler(CustomDetailViewGroupBehaviour_ItemCreated);
}

protected override void OnDeactivating()
{
// Unsuscribe LayoutManager’s ItemCreated event handler
((WinLayoutManager)((View as DetailView).LayoutManager)).ItemCreated -= new EventHandler(CustomDetailViewGroupBehaviour_ItemCreated); base.OnDeactivating();
}

void CustomDetailViewGroupBehaviour_ItemCreated(object sender, ItemCreatedEventArgs e)
{
// For every new item created in the DetailView, check if it’s a layout item group
if (e.Item is LayoutControlGroup)
{
LayoutControlGroup lLayoutControlGroup = (LayoutControlGroup)e.Item;

//If a group is being created, check if ExpandButtonVisible needs to be visible
//(i.e. only for non-nested groups with text located on top)

if (((lLayoutControlGroup.TextLocation == DevExpress.Utils.Locations.Default
lLayoutControlGroup.TextLocation == DevExpress.Utils.Locations.Top))
&& !(lLayoutControlGroup.Parent != null))
lLayoutControlGroup.ExpandButtonVisible = true;
}
}
}

On the other hand, you can check for specific groups too, by checking its Text property replacing previous If block with the following code:

if (lLayoutControlGroup.Text.StartsWith("ContactDetails"))

And that’s what you get when you run your application with the new ViewController in place:

Expanded groups:


Contact groups collapsed: