📜 ⬆️ ⬇️

Data Filters on ASP.NET MVC and Entity Framework

Very often, in various web applications, we work with data selected from the database tables. And it is often necessary to provide the user with the ability to filter this data. You can, of course, for each case collect data from the form manually and manually create the corresponding query for them. But what if we have 20 different pages representing those or other data? Handle everything for a long time and is not convenient, and support is even worse. My version of the solution to this problem on the ASP.NET MVC + Entity Framework under the cat.

I just want to note that my decision does not pretend to genius and an ideal architecture, there are minuses in it, which I will note at the end, and I really hope that the habr-community will point me to those minuses that I do not see.

I think it is worth giving an example of the use of what I describe below. The idea is this: suppose we have such a data model

public class Student { [Key] public int Id { get; set; } //  public string Name { get; set; } //  public string Group { get; set; } //   public short DefYear { get; set; } //    public bool IsDefended { get; set; } } 

')
The user works with the list of students and wants to select certain by name, group, year of protection and state of protection. To do this, we will need to describe the model of the filter, it will look like this.

 public class StudentFilter : FilterModel<Student> { [FilterName("")] [InputType(InputTypeAttribute.InputTypies.TextBox)] public string Name { get; set; } [FilterName("")] [InputType(InputTypeAttribute.InputTypies.TextBox)] public string Group { get; set; } [FilterName(" ")] [InputType(InputTypeAttribute.InputTypies.DropDownList)] [SelectListProperty("DefYearList", "")] public short? DefYear { get; set; } [FilterName("  ")] [InputType(InputTypeAttribute.InputTypies.DropDownList)] [SelectListProperty("IsDefendedList", "")] public bool? IsDefended { get; set; } public SelectList DefYearList { get { return new SelectList(new List<short> { 2011, 2012 }, DefYear); } } public SelectList IsDefendedList { get { var list = new List<object> { {new {Id="true", Title=""}}, {new {Id="false", Title=" "}}, }; return new SelectList(list, "Id", "Title", IsDefended); } } public override Func<Student, bool> FilterExpression { get { return p => (String.IsNullOrEmpty(Name) || p.Name.IndexOf(Name) == 0) && (String.IsNullOrEmpty(Group) || p.Group.ToLower().IndexOf(Group.ToLower()) == 0) && (DefYear == null || p.DefYear == DefYear) && (IsDefended == null || (p.IsDefended == IsDefended)); } } public override void Reset() { Name = null; Group = null; DefYear = null; IsDefended = null; } } 


Having a filter model, we can select from the database data that satisfies the values ​​stored in this model. You can do it like this

 var filters = IocContainer.Resolve<IFilterFactory>(); var students = DbContext.Students .Where(Filter.FilterExpression) .OrderBy(s => s.Name) .ToList(); 


And of course, the view code that will display the form that the user needs to fill out in order to filter the data.

 .................. <h2></h2> @Html.FilterForm(Filters.Find<UserFilter>()) .................. 


Filters this is the same filter repository that was used when sampling from the database. I personally get it with Unity.

Well and everything, more we will need nothing. The user will see in front of him about this form
image
And the data set that fits this form.

We now turn to the description of all that serves the above code. We start with the data model for filters. Since for each data presentation on the site, the filters may be different and, accordingly, different models, it is necessary to define its interface, which we will implement. The model for the filter should be able to generate the expression for selection from the data framework of the entity framework and should be able to set its initial state, so the interface, or rather the abstract class, has the following:

 public abstract class FilterModel { [Dependency] public IUnityContainer IocContainer { get; set; } public abstract void Reset(); } public abstract class FilterModel<ModelType> : FilterModel { public abstract Func<ModelType, bool> FilterExpression { get; } } 


I use the abstract class here because I wanted to always keep the Unity container at hand. Therefore, the IocContainer property can be safely removed and turned these two classes into ordinary interfaces.

Now that we have a model, we need to store it somewhere. Consider now the interface of the storage we need.

 public interface IFilterFactory { /// <summary> ///     /// </summary> /// <typeparam name="TFilter"></typeparam> /// <returns></returns> TFilter Find<TFilter>() where TFilter : FilterModel; /// <summary> ///    GUID  /// </summary> /// <param name="guidString"></param> /// <returns></returns> FilterModel Find(String guidString); /// <summary> ///    /// </summary> /// <param name="filter"></param> void Replace(FilterModel filter); /// <summary> ///       /// </summary> /// <param name="guidString">guid  </param> void Reset(String guidString); } 


We will need to search for a filter by type guid to set data to a specific type, when this data comes along with the request. Those. from the client side, we indicate to the server code, which filter to work with, by sending a guid of the type of this filter.

Here is the basic implementation of the IFilterFactory interface.

 public class DefaultFilterFactory : IFilterFactory { protected List<object> Collection { get; private set; } [InjectionConstructor] public DefaultFilterFactory() { Collection = new List<object>(); } [Dependency] public IUnityContainer IocContainer { get; set; } /// <summary> ///    /// </summary> /// <typeparam name="TFilter"> </typeparam> /// <returns></returns> public TFilter Find<TFilter>() where TFilter : FilterModel { try { return (TFilter)Collection.Single(f => f.GetType().FullName == typeof(TFilter).FullName); } catch { AddNew<TFilter>(); return Find<TFilter>(); } } /// <summary> ///    GUID   /// </summary> /// <param name="guidString">  GUID</param> /// <returns></returns> public FilterModel Find(String guidString) { return (FilterModel)Collection.Single(f => f.GetType().GUID.ToString() == guidString); } public void Replace(FilterModel filter) { try { var old = Collection.SingleOrDefault(f => f.GetType().FullName == filter.GetType().FullName); if (old != null) { if (!Collection.Remove(old)) throw new InvalidOperationException("    "); } } catch (InvalidOperationException) { throw; } catch { //    } Collection.Add(filter); } /// <summary> ///    /// </summary> /// <typeparam name="TFilter"> </typeparam> protected void AddNew<TFilter>() where TFilter : FilterModel { var filter = IocContainer.Resolve<TFilter>(); filter.Reset(); Collection.Add(filter); } /// <summary> ///    GUID   /// </summary> /// <param name="guidString">  GUID</param> /// <returns></returns> public void Reset(String guidString) { try { var filter = Find(guidString); filter.Reset(); } catch { //    } } } 


It is necessary to store the class object in the DefaultFilterFactory within a session in order to save the values ​​selected by the user. To do this, I use Unity with added lifemanager for ASP.NET MVC sessions, you can use any other DI framework or work with the object yourself. You can also write another implementation of the IFilterFactory interface that will use for storing xml for example or a database, here the fantasy can be unlimited ...

Next, you need to somehow collect the model from the user's request and place it in the repository. For this, we will use a special model builder (I confess here, I did not think it out, it would be better to inherit it from IModelBinder and use it like any other model builder in MVC)

 public class FilterBinder { /// <summary> ///   ,      guid  /// </summary> public static string TypeKey { get { return "_filter_type"; } } /// <summary> ///    ,       ///    ,         InputTypeAttribute /// </summary> public static Func<PropertyInfo, bool> IsFilterProperty { get { return p => p.GetCustomAttributes(true).Count(a => a.GetType() == typeof(InputTypeAttribute)) > 0; } } /// <summary> ///   /// </summary> public HttpRequest Request { get; protected set; } /// <summary> /// Unity  /// </summary> [Dependency] public IUnityContainer IocContainer { get; set; } /// <summary> ///   /// </summary> public Type FilterType { get { return IocContainer.Resolve<IFilterFactory>().Find(Request[TypeKey]).GetType(); } } public FilterBinder() { Request = HttpContext.Current.Request; } /// <summary> ///   /// </summary> /// <returns></returns> public FilterModel BindFilter() { var filter = (FilterModel)IocContainer.Resolve(FilterType); //    ,  InputTypeAttribute foreach (var property in FilterType.GetProperties().Where(FilterBinder.IsFilterProperty)) { object value = null; //   ,        ,     if (property.PropertyType == typeof(String)) { value = Request[property.Name]; } //  ,    else { try { var parse = property.PropertyType.GetMethod("Parse", new Type[] { typeof(String) }) ?? property.PropertyType.GetProperty("Value").PropertyType.GetMethod("Parse", new Type[] { typeof(String) }); if (parse != null) value = parse.Invoke(null, new object[] { Request[property.Name] }); } catch { value = null; } } //     property.SetValue(filter, value, null); } return filter; } } 


The code of the attributes involved in the collector will be given below, when we will be engaged in displaying the form of filters. Now I’ll give the controller code that will receive the filter data from the client.

 public class _FilterController : ExpandController { public ActionResult SetFilter(string backUrl) { try { var filter = IocContainer.Resolve<FilterBinder>().BindFilter(); if (filter == null) { FlashMessanger.ErrorMessage = "  "; return Redirect(backUrl); } Filters.Replace(filter); } catch { FlashMessanger.ErrorMessage = "  :   . ,  "; } return Redirect(backUrl); } public ActionResult Reset(string backUrl, string filter) { Filters.Reset(filter); return Redirect(backUrl); } } 

(ExpandController - Extended Base Controller, FlashMessanger - add-on over TempData, I do not give explanations on this matter, since this is a completely different story)

As can be seen from the code, there are two actions in the controller: one deals with the processing of values, the second with their reset. Both, after the end of their mission, return the user to the page where the request came from. And on it the user can already see the filtered set of data of interest.

Having written all the business logic of the task, now you can turn on the display of all this disgrace to the user. Since while I used these filters in just one project, the html generates a helper for me, and there is almost no flexibility of markup. If you have any ideas how to solve this problem, I will be glad to hear them in the comments.

In order to make the display helper a bit smarter, we will use the attributes for our filters, which we will mark the properties of the model. Here is their code

 /// <summary> ///   ,   SelectList    /// </summary> public class SelectListPropertyAttribute : Attribute { /// <summary> ///   /// </summary> public string Property { get; set; } /// <summary> ///   null /// </summary> public string OptionLabel { get; set; } public SelectListPropertyAttribute(string property) { Property = property; OptionLabel = String.Empty; } public SelectListPropertyAttribute(string property, string optionLabel) : this(property) { OptionLabel = optionLabel; } } /// <summary> ///               ///          /// </summary> public class InputTypeAttribute : Attribute { public enum InputTypies { TextBox, CheckBox, DropDownList } public InputTypies Type { get; set; } public InputTypeAttribute(InputTypies type) { Type = type; } } /// <summary> ///       /// </summary> public class FilterNameAttribute : Attribute { public string Name { get; set; } public FilterNameAttribute(string name) { Name = name; } } 


and helper code

 public static class Helpers { /// <summary> ///  ,    /// </summary> private const string CONTROLLER_NAME = "_Filter"; /// <summary> ///      /// </summary> private const string SET_ACTION_NAME = "SetFilter"; /// <summary> ///      /// </summary> private const string RESET_ACTION_NAME = "Reset"; /// <summary> ///    /// </summary> /// <returns></returns> public static IHtmlString FilterForm(this HtmlHelper helper, FilterModel filter, string controllerName = CONTROLLER_NAME, string setActionName = SET_ACTION_NAME, string resetActionName = RESET_ACTION_NAME) { var url = new UrlHelper(HttpContext.Current.Request.RequestContext, helper.RouteCollection); // url      var hrefSet = url.Action(setActionName, controllerName, new { area = "", backUrl = HttpContext.Current.Request.Url }); var result = String.Format("<form action=\"{0}\" method=\"post\">\n\t<div class=\"filters\">", hrefSet); result += helper.Hidden(FilterBinder.TypeKey, filter.GetType().GUID.ToString()).ToString(); result += "<h3></h3>"; //  ,    //    Func<PropertyInfo, string> getFilterName = p => p.GetCustomAttributes(typeof(FilterNameAttribute), true).Any() ? ((FilterNameAttribute)p.GetCustomAttributes(typeof(FilterNameAttribute), true).Single()).Name : p.Name; //  ,  SelectList   Func<PropertyInfo, PropertyInfo> getSelectListProperty = p => !p.GetCustomAttributes(typeof(SelectListPropertyAttribute), true).Any() ? null : p.DeclaringType.GetProperty( ((SelectListPropertyAttribute)p.GetCustomAttributes(typeof(SelectListPropertyAttribute), true).Single()).Property, typeof(SelectList) ); //      Func<PropertyInfo, string> getSelectListOptionLabel = p => !p.GetCustomAttributes(typeof(SelectListPropertyAttribute), true).Any() ? null : ((SelectListPropertyAttribute)p.GetCustomAttributes(typeof(SelectListPropertyAttribute), true).Single()).OptionLabel; //    ,  InputTypeAttribute foreach (var property in filter.GetType().GetProperties().Where(FilterBinder.IsFilterProperty)) { result += "\n\t\t<div class=\"filter_item\">"; result += "\n\t\t\t<span>" + getFilterName(property) + "</span>"; //   ,       var type = (InputTypeAttribute)property.GetCustomAttributes(typeof(InputTypeAttribute), true).Single(); //  html  switch (type.Type) { case InputTypeAttribute.InputTypies.TextBox: result += helper.TextBox(property.Name, property.GetValue(filter, null)).ToString(); break; case InputTypeAttribute.InputTypies.CheckBox: result += helper.CheckBox(property.Name, property.GetValue(filter, null)).ToString(); break; case InputTypeAttribute.InputTypies.DropDownList: var selectList = getSelectListProperty(property) != null ? (SelectList)getSelectListProperty(property).GetValue(filter, null) : new SelectList(new List<object>()); result += String.IsNullOrEmpty(getSelectListOptionLabel(property)) ? helper.DropDownList(property.Name, selectList) : helper.DropDownList(property.Name, selectList, getSelectListOptionLabel(property)); break; } result += "\n\t\t</div>"; } result += "\n\t\t<div class=\"clear\"></div>"; result += String.Format( @"<input type='image' src='{0}' /><a href='{1}'><img src='{2}' alt='' /></a>", url.Content("~/Content/images/button_apply.png"), url.Action(resetActionName, controllerName, new { area = "", backUrl = HttpContext.Current.Request.Url, filter = filter.GetType().GUID }), url.Content("~/Content/images/button_cancel.png") ); return helper.Raw(result + "\n\t</div>\n</form>"); } } 


Perhaps that's all. Now back to the minuses that I mentioned at the beginning:

1) If the user opens the page, then leaves it for a time sufficient for the session to expire, then fills in the form of filters and sends it, then disappointment awaits him. The system cannot find the filter type by its guid. How to solve the problem, I have not yet invented it, but I am actively thinking about it

2) As I said, the helper generates static markup, which cannot be corrected in views. For this we get a not very flexible display. Although of course you can refuse to use the helper and for each model to describe the markup form manually in the views, but this is a bit tedious.

3) For the FilterModel class Reset method, you can write a default implementation in which all properties marked with InputTypeAttribute will be set to zero values.

4) I am not sure that the architecture of all this is absolutely correct.

Thank you so much for reading my article to the end! I would be extremely grateful to everyone who will leave their opinions and comments.

UPDATE
As correctly noted in the comments

 public abstract class FilterModel<ModelType> : FilterModel { public abstract Func<ModelType, bool> FilterExpression { get; } } 


better to replace

 public abstract class FilterModel<ModelType> : FilterModel { public abstract Expression<Func<ModelType, bool>> FilterExpression { get; } } 


In this case, the filter expression will be transformed into a SQL query.

Source: https://habr.com/ru/post/141440/


All Articles