Tuesday, March 27, 2018

How to: Implement Cascading Filtering for Lookup List Views

How to: Implement Cascading Filtering for Lookup List Views


[DataSourceProperty("MasterfieldName.EntityNames")]

Most applications have Detail Views which contain Lookup Property Editors that need to be filtered. Often, you should make the List View data source of these editors dependent on values of other Property Editors located in the same Detail View. For this purpose, the eXpressApp Framework provides the DataSourcePropertyAttribute and DataSourceCriteriaAttribute. This topic demonstrates how to use both these attributes in code and using the Application Model. In these examples, the Order, Product and Accessory classes will be implemented in different ways.
Show Me
A complete sample project is available in the DevExpress Code Examples database at http://www.devexpress.com/example=E218.

Expanded Initial Implementation

Initially, an Order includes a Product and Accessory. In addition, each Accessory is related to a particular Product. So, the Product and Accessory objects are related by a One-to-Many relationship. The following code shows how these classes can be implemented:
C#
VB
using System.ComponentModel;
using DevExpress.Data.Filtering;
//... 
[DefaultClassOptions]
public class Order : BaseObject {
   public Order(Session session) : base(session) { }
   private Product product;
   public Product Product {
      get {
         return product;
      }
      set {
         SetPropertyValue("Product", ref product, value);
      }
   }
   private Accessory accessory;
   public Accessory Accessory {
      get {
         return accessory;
      }
      set {
         SetPropertyValue("Accessory", ref accessory, value);
      }
   }
}
public class Product : BaseObject {
   public Product(Session session) : base(session) { }
   private String productName;
   public String ProductName {
      get { 
         return productName;
      }
      set {
         SetPropertyValue("ProductName", ref productName, value);
      }
   }
   [Association("P-To-C")]
   public XPCollection Accessories {
      get { return GetCollection("Accessories"); }
   }
}
public class Accessory : BaseObject {
   public Accessory(Session session) : base(session) { }
   private String accessoryName;
   public String AccessoryName {
      get { 
         return accessoryName; 
      }
      set {
         SetPropertyValue("AccessoryName", ref accessoryName, value);
      }
   }
   private bool isGlobal;
   public bool IsGlobal {
      get {
         return isGlobal;
      }
      set {
         SetPropertyValue("IsGlobal", ref isGlobal, value);
      }
   }
   private Product product;
   [Association("P-To-C")]
   public Product Product { 
      get {
         return product;
      } 
      set {
         SetPropertyValue("Product", ref product, value);
      } 
   }
}
The image below demonstrates the Order Detail View in a Windows Forms application:

Here, the Product and Accessory Lookup Property Editors provide the entire Product and Accessory objects collections. However, certain scenarios may require a filtered collection to be displayed within the Accessory Lookup Property Editor.

Expanded Scenario 1 - Populate the Lookup with Objects from the Specified Collection Property

In the Order Detail View, it would be convenient if the Accessory Lookup Property Editor only provided the Accessory objects related to the currently selected Product. To accomplish this, the DataSourcePropertyAttribute attribute can be used. This attribute's value specifies a property whose possible values serve as the data source for the current lookup property. For the Order class' Accessory property, this attribute should be set to the Product class' Accessory property:
C#
VB
[DefaultClassOptions]
public class Order : BaseObject {
   // ... 
   [DataSourceProperty("Product.Accessories")]
   public Accessory Accessory {
      get {
         return accessory;
      }
      set {
         SetPropertyValue("Accessory", ref accessory, value);
      }
   }
}
The following image demonstrates the resulting Order Detail View:

The same result can be achieved if you specify the DataSourceProperty property of the Application Model's Application | BOModel | Class | Member node.
Note
  • An independent server-mode collection is created as a data source for a lookup ListView when the IModelListView.DataAccessMode option is set to Server or InstantFeedback for that ListView model. However, when the DataSourcePropertyAttribute is applied for the lookup property, the property pointed in the attribute is used as a lookup data source, and the standalone independent server-mode collection is not created and is not used at all. The reason is that the data source property getter contains logic calculated on the client side and the data source is populated by the client application at the moment when the lookup editor requests to display objects. So, the Server or InstantFeedback mode option and the DataSourceProperty attribute do not work simultaneously.
  • To use the approach demonstrated above for Entity Framework objects with a many-to-many relationship implemented through an intermediate object, use the Map method to specify the join table, its columns names, and to apply additional configuration. To learn more, refer to the Entity Framework Fluent API - Relationships topic.

Expanded Scenario 2 - Apply Criteria For the Lookup Property Collection

It may be necessary for a Lookup List View to contain objects whose properties satisfy a specified criteria. For example, there can be Accessories that can be chosen for every Product, so called Global Accessories. To display these Global Accessories in the Order Detail View, the DataSourceCriteriaAttribute attribute can be used. This attribute's value specifies the required criteria:
C#
VB
[DefaultClassOptions]
public class Order : BaseObject {
   // ... 
   [DataSourceCriteria("IsGlobal = true")]
   public Accessory Accessory {
      get {
         return accessory;
      }
      set {
         SetPropertyValue("Accessory", ref accessory, value);
      }
   }
}
public class Accessory : BaseObject {
      // ... 
   private bool isGlobal;
   public bool IsGlobal {
      get { return isGlobal; }
      set { 
         SetPropertyValue("IsGlobal", ref isGlobal, value);      
      }
   }
}

The same result can be achieved if you specify the DataSourceCriteria property of the Application Model's BOModel | Class | Member node.

Expanded Scenario 3 - Use Alternate Criteria if the Specified Data Source Property is Empty

Let a Lookup Property data source depend on a value of another property (see Scenario1). When this value is not specified, you can provide another data source. For example, when a Product is not specified in the Product Lookup Property Editor of the Order Detail View, the Accessories Lookup Property Editor will provide the collection of Global Accessories. To accomplish this, the DataSourcePropertyIsNullMode and DataSourcePropertyIsNullCriteria parameters can be specified for the DataSourceProperty attribute.
C#
VB
[DefaultClassOptions]
public class Order : BaseObject {
   // ... 
   [DataSourceProperty("Product.Accessories", 
      DataSourcePropertyIsNullMode.CustomCriteria, "IsGlobal = true")]
   public Accessory Accessory {
      get {
         return accessory;
      }
      set {
         SetPropertyValue("Accessory", ref accessory, value);
      }
   }
}

Note
In addition to the CustomCriteria value, the DataSourcePropertyIsNullMode parameter of the DataSourceProperty attribute can also have the SelectAll and SelectNothing values. In this instance, you do not need to specify the DataSourcePropertyIsNullCriteria parameter.
The same result can be achieved if you specify the DataSourcePropertyDataSourcePropertyIsNullMode and DataSourcePropertyIsNullCriteria properties of the Application Model's BOModel | Class | Member node.

Expanded Scenario 4 - Populate the Lookup Manually

Assume the Accessory Lookup Property data source can contain Global Accessories (see Scenario 2) in addition to the currently selected Product's Accessories (see Scenario 1). In addition, assume there is a flag called IncludeGlobalAccessories. If it is in effect, the Accessory Lookup Property data source is composed of both the current Product's Accessories and Global Accessories. Otherwise, it is made up of the current Product's Accessories only. For this purpose, an additional collection must be shown for the Accessory property. This collection must be refreshed each time the Product or IncludeGlobalAccessories property is changed. The following code demonstrates how to implement the Order class for this task.
This approach cannot be implemented in a Mobile application, because the Mobile platform does not support non-persistent Collection Properties.
C#
VB
[DefaultClassOptions]
public class Order : BaseObject {
   // ... 
   // Set the AvailableAccessories collection as a data source for the Accessory property 
   [DataSourceProperty("AvailableAccessories")] 
   public Accessory Accessory {
      get {return accessory;}
      set {
         SetPropertyValue("Accessory", ref accessory, value);
      }
   }
   private XPCollection availableAccessories;
   [Browsable(false)] // Prohibits showing the AvailableAccessories collection separately 
   public XPCollection AvailableAccessories {
      get {
         if(availableAccessories == null) {
            // Retrieve all Accessory objects 
            availableAccessories = new XPCollection(Session);
            // Filter the retrieved collection according to current conditions 
            RefreshAvailableAccessories();
         }
         // Return the filtered collection of Accessory objects 
         return availableAccessories;
      }
   }
   private void RefreshAvailableAccessories() {
      if(availableAccessories == null)
         return;
      // Process the situation when the Product is not specified (see the Scenario 3 above) 
      if(Product == null) {
         // Show only Global Accessories when the Product is not specified 
         availableAccessories.Criteria = CriteriaOperator.Parse("[IsGlobal] = true");
      }
      else {
         // Leave only the current Product's Accessories in the availableAccessories collection 
         availableAccessories.Criteria = new BinaryOperator("Product", Product);
         if(IncludeGlobalAccessories == true) {
            // Add Global Accessories 
            XPCollection availableGlobalAccessories = 
               new XPCollection(Session);
            availableGlobalAccessories.Criteria = CriteriaOperator.Parse("[IsGlobal] = true");
            availableAccessories.AddRange(availableGlobalAccessories);
         }
      }
      // Set null for the Accessory property to allow an end-user  
      //to set a new value from the refreshed data source 
      Accessory = null;
   }
   public Product Product {
      get {return product;}
      set {
         SetPropertyValue("Product", ref product, value);
         // Refresh the Accessory Property data source 
         RefreshAvailableAccessories();
      }
   }
   private bool includeGlobalAccessories;
   [ImmediatePostData] //Use this attribute to refresh the Accessory  
   public bool IncludeGlobalAccessories {
      get {return includeGlobalAccessories;}
      set {
         if(includeGlobalAccessories != value) {
            includeGlobalAccessories = value;
            if(!IsLoading) {
               // Refresh the Accessory Property data source                     
               RefreshAvailableAccessories();
               SetPropertyValue("IncludeGlobalAccessories", ref includeGlobalAccessories, value);
            }
         }
      }
   }
}
The following image shows an Order Detail View:

Note
This approach to filter Lookup List Views is helpful when you cannot write the required criteria as a string to pass it as an attribute's parameter. In code, you can use any criteria operator and any operands (including Function Criteria Operators).

Expanded Scenario 5 - Filter the Lookup Property Collection Based on the Current Object's Properties

To access the currently edited record in criteria passed to the DataSourcePropertyAttribute, use the Current Object Parameter(@This). With the code below, the Contact.Manager lookup will display all Contacts with the "Manager" Position except for the current Contact.
C#
VB
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl;
// ... 
public class Contact : Person {
    // ... 
    [DataSourceProperty("Department.Contacts", DataSourcePropertyIsNullMode.SelectAll)]
    [DataSourceCriteria("Position.Title = 'Manager' AND Oid != '@This.Oid'")]
    public Contact Manager {
        get {
            return manager;
        }
        set {
            SetPropertyValue("Manager", ref manager, value);
        }
    }
    // ... 
}
Note
This code snippet is not included in the linked How to Filter Lookup List Views example. Instead, it is demonstrated in the %PUBLIC%\Documents\DevExpress Demos 17.2\Components\eXpressApp Framework\MainDemo\CS\MainDemo.Module\BusinessObjects\Contact.csfile of Main Demo solution shipped with XAF. You can also see a similar code in the Implement Dependent Reference Properties (XPO) tutorial.

Expanded See Also

No comments: