Wednesday, 16 January 2013

Adding Multi Column Sort to Dynamic Data List Page

Well this is the first post of 2013 and has been brewing for a while, what we want is to be able to set the initial sort of the GridView on the List page or ListDetails page. Listing 1 shows what we want to achieve.

OrderDate sorted Descending and ShipName sorted Ascending

Figure 1 – OrderDate sorted Descending and ShipName sorted Ascending

The Multi Column Sort Attribute

Figure 2 shows the first version of the attribute, I was using magic strings. But in Figure 3 we have an enum from System.Web.UI.WebControls to help us out (I’m a bad speller and I tend to type too quickly) so I think that is better.

Early Version Of Metadata

Figure 2 – Early Version Of Metadata

Example Metadata

Figure 3 – Example Metadata

Listing 1 shows the MultiColumnSortAttribute I am using the params construct to get a list of values from the attribute and I am using the BuildColumnParametersDictionary method to convert the object array to a dictionary with a string key the column name and a value of the SortDirection.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class MultiColumnSortAttribute : Attribute
{
private IDictionary<String, SortDirection> _columns;

public IDictionary<String, SortDirection> Columns
{
get { return this._columns; }
}

public MultiColumnSortAttribute(params object[] columnParameters)
{
_columns = BuildColumnParametersDictionary(columnParameters);
}

private IDictionary<String, SortDirection> BuildColumnParametersDictionary(object[] objArray)
{
var dictionary = new Dictionary<String, SortDirection>();
if ((objArray != null) && (objArray.Length != 0))
{
if ((objArray.Length % 2) != 0)
throw new InvalidOperationException(
String.Format("Need even number of control parameters.",
new object[0]));

for (int i = 0; i < objArray.Length; i += 2)
{
if (objArray[i] is String && objArray[i + 1] is SortDirection)
{
if (objArray[i] == null)
throw new InvalidOperationException(
String.Format("Column name [{0}] is null.", i));

// get column name from array
String columnName = objArray[i] as String;
if (columnName == null)
throw new InvalidOperationException(
String.Format("Column name '{0}' is not a String.",
objArray[i].ToString()));

// check for duplicate column names
if (dictionary.ContainsKey(columnName))
throw new InvalidOperationException(
String.Format("Column name '{0}' occurs more than once.",
columnName));

// get sort direction from array
if (objArray[i + 1] == null)
throw new InvalidOperationException(
String.Format("Sort direction '{0}' is not a of type SortDirection.",
objArray[i].ToString()));

SortDirection sortDirection = (SortDirection)objArray[i + 1];
dictionary[columnName] = sortDirection;
}
}
}
return dictionary;
}
}

Listing 1 - MultiColumnSortAttribute

An Extension Method to Add Multi Column Sort to the Page

This extension method in Listing 2 is a little more complicated than it would need to be to handle just the Multi Column Sort, it also handles the DisplayColumn attribute to, this way we cover all bases. 

Note: The GetAttribute<T>() extension method is covered here Writing Attributes and Extension Methods for Dynamic Data
/// <summary>
/// Sets the initial sort order.
/// </summary>
/// <param name="queryExtender">The query extender.</param>
/// <param name="table">The table.</param>
public static void SetInitialSortOrder(this QueryExtender queryExtender, MetaTable table)
{
var multiColumnSort = table.GetAttribute<MultiColumnSortAttribute>();
if (multiColumnSort != null)
{
var firstEntry = multiColumnSort.Columns.Keys.First();
var order = new OrderByExpression()
{
DataField = firstEntry,
Direction = multiColumnSort.Columns[firstEntry],
};

foreach (var item in multiColumnSort.Columns)
{
if (item.Key != firstEntry)
{
order.ThenByExpressions.Add(new ThenBy()
{
DataField = item.Key,
Direction = item.Value
});
}
}
queryExtender.Expressions.Add(order);
}
else if (table.SortColumn != null)
{
var order = new OrderByExpression()
{
DataField = table.SortColumn.Name,
Direction = table.SortDescending ? SortDirection.Descending : SortDirection.Ascending,
};
queryExtender.Expressions.Add(order);
}
}

Listing 2 – SetInitialSortOrder Extension Method

Using in the List Page Template

All we need to do in the List and ListDetails page templates is to add one line to the Page_Init method.

protected void Page_Init(object sender, EventArgs e)
{
table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
GridView1.SetMetaTable(table, table.GetColumnValuesFromRoute(Context));
GridDataSource.EntityTypeFilter = table.EntityType.Name;

// set initial sort order
GridQueryExtender.SetInitialSortOrder(table);
}

Listing 3 – adding the extension method to the Page Template

Download

You may need to look through the list of files on my sky drive to find the one you want, it’s called MultiColumnSort.zip

4 comments:

Unknown said...

Hi,
have you try this with mysql datasource?

e can't filter with any Entity Key! :(
Got thi:
"Unable to cast object of type 'System.Web.DynamicData.MetaColumn' to type 'System.Web.DynamicData.MetaForeignKeyColumn'."

What are i doing wrong? In ModelMetadata, have this:
[MetadataTypeAttribute(typeof(trabalho.TrabalhoMetadata))]
[DisplayColumn("ID_Trabalhos", "ID_Trabalhos", true)]
[DisplayName("Trabalhos")]
public partial class trabalho
{
internal sealed class TrabalhoMetadata
{
[Display(Name = "ID", Order = 1)]
[FilterUIHint("ForeignKey")]
public int ID_Trabalhos { get; set; }
[FilterUIHint("Contains")]
[Display(Name = "Trabalho", Order = 2)]
public string NomeTrab { get; set; }

maybe it's simple, but could u help
Luís Faria

Stephen J. Naughton said...

As far as i know this only works with Entity Framework and Linq to SQL, you can however get an entity framework connector for MySql

Steve

jh said...

Hi, if I want to sort by a foreign key column, how would I reference that? For example, lets say I have an Orders table, but I want to sort by Customer Name, which is in a Customers table and related to Orders by Orders.CustID? In an example like this, what would my MultiColumnSort attribute need to be or is that not accounted for in the Attribute? Thanks for all your helpful articles!

Stephen J. Naughton said...

Hi jh, yes you would use the "Orders.Customer.CustomerName" I think this will work.

Steve.