How to: Implement Cascading Filtering for Lookup List Views

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.

Tip

A complete sample project is available in the DevExpress Code Examples database at http://www.devexpress.com/example=E218.

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:

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<Accessory> Accessories {
      get { return GetCollection<Accessory>("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:

LookupPropetyEditors_1

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.

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:

[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:

LookupPropertyEditors_2

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.

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:

[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);      
      }
   }
}

LookupPropertyEditors_3

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

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.

[DefaultClassOptions]
public class Order : BaseObject {
   // ...
   [DataSourceProperty("Product.Accessories", 
      DataSourcePropertyIsNullMode.CustomCriteria, "IsGlobal = true")]
   public Accessory Accessory {
      get {
         return accessory;
      }
      set {
         SetPropertyValue("Accessory", ref accessory, value);
      }
   }
}

LookupPropertyEditors_3

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 DataSourceProperty, DataSourcePropertyIsNullMode and DataSourcePropertyIsNullCriteria properties of the Application Model's BOModel | Class | Member node.

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.

[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<Accessory> availableAccessories;
   [Browsable(false)] // Prohibits showing the AvailableAccessories collection separately
   public XPCollection<Accessory> AvailableAccessories {
      get {
         if(availableAccessories == null) {
            // Retrieve all Accessory objects
            availableAccessories = new XPCollection<Accessory>(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<Accessory> availableGlobalAccessories = 
               new XPCollection<Accessory>(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:

FilterLookupDataSource

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).

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.

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 18.1\Components\eXpressApp Framework\MainDemo\CS\MainDemo.Module\BusinessObjects\Contact.cs file of Main Demo solution shipped with XAF. You can also see a similar code in the Implement Dependent Reference Properties (XPO) tutorial.

See Also