manureini / MComponents

Another blazor component library: Grid, Select, Wizard etc.
MIT License
66 stars 13 forks source link

QueryBuilder #14

Closed agriffard closed 2 days ago

agriffard commented 9 months ago

What is exactly your purpose with the MQueryBuilder?

The sample does not go beyond declaring the fields you want to be able to filter on.

How do you declare an instance of the field in the page and then, how are you able in the code to retrieve the actual selected filters with their field, operator and value?

Thank you for your answer.

manureini commented 9 months ago

Hello ;)

Thanks for you question! I'm really happy when someone is interested in this library and may using it.

I'm using the Query Builder in a wasm application, but it will also work on server side. In my case, there a people which can be filtered by a custom query. I've defined 3 different values, which can be combined with operators. I'm calling them Rule, but it's (in a simple use case) just the part of a rule without an operator.

RuleName="FirstName" defines the FirstName Rule with PropertyType="typeof(string)".

MQueryBuilderComplexField is an example for a dropdown of some values (in this case objects of class Event).

<MQueryBuilder T="object" @bind-RuleGroup="mModel.RuleGroup" @bind-RuleGroup:after="OnRulesChanged">

      <MQueryBuilderField RuleName="FirstName" Title="@L[nameof(Loc.FirstName)]" PropertyType="typeof(string)" />
      <MQueryBuilderField RuleName="LastName" Title="@L[nameof(Loc.LastName)]" PropertyType="typeof(string)" />

      <MQueryBuilderComplexField T="Guid" RuleName="EventEmployed" Title="@L[Loc.RuleEventEmployed]" AllowedOperators="new MQueryBuilderConditionOperator[0]">
          <FormTemplate>
              <MSelect T="Event" Property="@nameof(Event.Name)" Options="mEvents" Value="mEvents.FirstOrDefault(e => e.Id == context.Value)" ValueChanged="async v => { await context.ValueChanged.InvokeAsync(v.Id); }" ValueExpression="@(RenderHelper.GetFakePropertyInfoExpression<Event>(string.Empty, "Id"))" />
          </FormTemplate>
      </MQueryBuilderComplexField>

  </MQueryBuilder>

In OnRulesChanged I serialize the MQueryBuilderRuleGroup mModel.RuleGroup property and send it to a controller. If you are on ServerSide you can skip this step.

In the controller I do something like this:

 var people = mEfContext.Set<Person>().Where(p => p.OwnerId != null);

  var query = MQueryBuilderHelper.ApplyRules(people, pRuleGroup, (c) =>
  {
      return c switch
      {
          "FirstName" => (u, v) => u.FirstName.ToLower(),
          "LastName" => (u, v) => u.LastName.ToLower(),
          "EventEmployed" => (p, v) => efContext.Set<Employment>().Any(a => a.PersonId == p.Id && (object)a.EventId == v)
      };
  });

var result = query.ToArray();

So we prepare a IQueryable (in this case my database table), the user configured MQueryBuilderRuleGroup and a callback, which will map RuleName to an part of a query. With this information the MQueryBuilderHelper will add the corresponding filter to the queryable.

There is a small trick implemented: "FirstName" => (u, v) => u.FirstName.ToLower(), Here the parameter v is not used. So MQueryBuilderHelper assumes, that there is an operator missing. This is ok, because the user can select it and it will be applied by the helper.

"EventEmployed" => (p, v) => efContext.Set<Employment>().Any(a => a.PersonId == p.Id && (object)a.EventId == v) In this case the parameter v is used. So no operator can be used in this case. Thats why they are disabled in the ui AllowedOperators="new MQueryBuilderConditionOperator[0]" For example in this case people needs an employment of a selected event to be in the result.

Fell free to ask any questions. I'd be happy to help you ;)

manureini commented 9 months ago

image

also see: https://github.com/manureini/MComponents/commit/e82547f7454a01add89a82b8f90fb9103c09d8a7

agriffard commented 9 months ago

Thank you for the sample you added.

I recently used this Grid component https://demos.blazorbootstrap.com/grid#server-side-filtering-paging-sorting and I find interesting the way the filters are exposed and how you can use them to build your query.

https://github.com/vikramlearning/blazorbootstrap/blob/ba7a76afce7e190f8b6c85c53e1e97fe6f42e782/BlazorBootstrap.Demo.RCL/Services/CustomerService.cs#L23

https://github.com/vikramlearning/blazorbootstrap/blob/ba7a76afce7e190f8b6c85c53e1e97fe6f42e782/blazorbootstrap/Models/GridDataProviderRequest.cs#L87

In this case, the filters are under each table column header but I would love to be able to build my filters outside the grid with a component like the one you are providing.

I use EF Core and https://github.com/alirezanet/Gridify to dynamicly query SQL. Thanks to it, I can either use LINQ expressions or an interpreted string to filter an IQueryable.

manureini commented 9 months ago

Oh yes I also had to solve this issue. I want to use server side filtering for my projects, too.

I've implemented an Odata adapter and my Simple.Odata.Client fork https://github.com/manureini/MComponents.Simple.Odata.Client/blob/master/MComponents.Simple.Odata.Client/ListViews/ListView.cs (inherit from this class and use the DataAdapter for MGrids) on the client side and I'm using AspNetCoreOData. But yes it's somehow working, and right now I'm ok with this solution, but honestly not 100% happy ;). I want to have a better working local data cache, but my version is not very advanced.

Oh nice I did now know that something like Gridify exists. I'll keep an eye on it ;)

I'd be happy if you can give me some feedback ;)

agriffard commented 9 months ago

Do you plan to make a new release of the library with this component included?

manureini commented 8 months ago

Do you plan to make a new release of the library with this component included?

I've released a new version right now ;)