ALMMa / datatables.aspnet

Microsoft AspNet bindings and automatic parsing for jQuery DataTables along with extension methods to help on data queries.
MIT License
301 stars 136 forks source link

How best to pass AdditionalParameters in request? #50

Open spencerflagg opened 7 years ago

spencerflagg commented 7 years ago

Great library guys.

I didn't know where else to ask this, hopefully this is semi-appropriate... I'm trying to send column level custom filter data (date ranges, lookups, etc) back to the server.

I see that IDataTablesRequest can take an AdditionalParameters dictionary, but I'm not sure exactly how to structure my object on the client or where to pass it in. ajax.data perhaps? I'd love to see an example.

Thanks in advance for the help

spencerflagg commented 7 years ago

It should be noted that I've since found the Mvc5.AdditionalParameters project that was created last week, and have added the data and dataSrc properties to the ajax object as suggested, but I'm still not seeing AdditionalParameters come into my controller as anything but null.

ALMMa commented 7 years ago

Check out issue #47

As I've said there, this behavior is not optimal and will be improved but for now, you must enable the request parameters on the DataTables.AspNet options and provide a model binder which will parse your parameters.

It's not sufficient to simply send those parameters (at least, for now). I'll implement a automatic bindind but it's important to consider that some complex members might still require a custom binder nonetheless.

spencerflagg commented 7 years ago

Thanks! Excuse my ignorance, but I'm not sure what you mean by "enable the request parameters on the DataTables.AspNet options".

Thanks in advance for your time

ALMMa commented 7 years ago

Check code and comments on Global.asax for the additional parameters sample project. You have to explicitly set the DataTables.AspNet project to allow for additional request/response parameters, as well as provide a custom binder with the appropriate function to parse those additional parameters.

See more here: https://github.com/ALMMa/datatables.aspnet/blob/dev/samples/DataTables.AspNet.Samples.Mvc5.AdditionalParameters/Global.asax.cs

var options = new DataTables.AspNet.Mvc5.Options()
            .EnableRequestAdditionalParameters()
            .EnableResponseAdditionalParameters();

var binder = new DataTables.AspNet.Mvc5.ModelBinder();
ParseAdditionalParameters = Parser;

AspNet.Mvc5.Configuration.RegisterDataTables(options, binder);

Parser is your function. On Global.asax there is a sample for that function.

spencerflagg commented 7 years ago

Thanks! What if you're using DataTables.AspNet.WebApi2 instead of DataTables.AspNet.Mvc5? Seems like the Parser function is considerably different.

ALMMa commented 7 years ago

Hello, @spencerflagg

Yes, you are correct. The parser function is different because WebApi provides different binding mechanism. For instance, instead of a ControllerContext, it provides a HttpActionContext.

You might want to check more about HttpActionContext here and more about ModelBindingContext here to correctly implement your parsing function.

This behavior is due to the very nature of the binding mechanism from each platform. Parser function will always return an IDictionary<string, object> but parameters will change to match those from the chosen platform (MVC, WebApi, AspNet Core).

danielearrighi commented 7 years ago

Hello, what do you think about this generic method to parse the additional parameter?

public static IDictionary<string, object> DataTablesParametersParser(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var collection = controllerContext.HttpContext.Request.QueryString;
var modelKeys = collection.AllKeys
    .Where(m => !m.StartsWith("columns"))
    .Where(m => !m.StartsWith("order"))
    .Where(m => !m.StartsWith("search"))
    .Where(m => m != "draw")
    .Where(m => m != "start")
    .Where(m => m != "length")
    .Where(m => m != "_"); //Cache bust

var dictionary = new Dictionary<string, object>();
foreach (string key in modelKeys)
{
    var value = bindingContext.ValueProvider.GetValue(key).AttemptedValue;
    if (value.Length > 0)   
        dictionary.Add(key, value);
}
return dictionary;
}

Basically, it filters all the "standard" querystrings passed by datatables itself, and parses out the remaining parameters in the dictionary. This way I can pass every parameters I want (should not start with columns, order, search of course) and I get it in the dictionary.

Like:

 "data": function (d) {
  d.buildingDate = '@Model.SearchBuildingDate';
  d.State = '@Model.SearchState';
}

What do you think? Any downsides using this approach?

Thanks

ALMMa commented 7 years ago

@danielearrighi Thanks!

That sounds interesting. The only downside I can think of if the multiple .Where, which could be replaced by a single (faster compiling/running) statement.

I'll sure give that a try. Maybe provide an automatic binder (with some automatic request parameters parsing) and a manual binder (with override capability to solve complex cases, like nested objects).

I just need to test, however, on POST. Dunno for sure right now but in some cases, after parsing body element, you cannot read values again. I have to double check that on all AspNet Core, MVC and WebApi to make sure that parsing parameters automatically won't break the main request params binding (eg: draw, columns, etc).

Tegawende commented 7 years ago

Hello,

I want to return additional parameters from my controllers. I enabled the response parameters according to what you said. My question is about the parser function. How can I write my parser function if I want to return a string array?

Sorry for my bad english. Thanks !

fabriciogs commented 7 years ago

@danielearrighi @ALMMa

Since I'm using POST to send data using DataTables, I modified @danielearrighi solution to something a little bit better:

public static IDictionary<string, object> DataTablesParametersParser(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
    var httpMethod = controllerContext.HttpContext.Request.HttpMethod;
    var collection = httpMethod.ToUpper().Equals("POST") ? controllerContext.HttpContext.Request.Form : controllerContext.HttpContext.Request.QueryString;
    var modelKeys = collection.AllKeys.Where(m => !m.StartsWith("columns") && !m.StartsWith("order") && !m.StartsWith("search") && m != "draw" && m != "start" && m != "length" && m != "_"); //Cache bust
    var dictionary = new Dictionary<string, object>();
    foreach (string key in modelKeys)
    {
        var value = bindingContext.ValueProvider.GetValue(key).AttemptedValue;
        if (value.Length > 0)
            dictionary.Add(key, value);
    }
    return dictionary;
}

IE: When HttpMethod is POST I grab Request.Form, else Request.QueryString.

The code isn't pretty, I know, but it works!

ronnieoverby commented 6 years ago

In my aspnet core project, I just did this:

// some.js
$('#myTable').DataTable({
    serverSide: true,
    ajax: {
        url: '/someAjaxUrl',
        data: function (d) {
            d.appJson = JSON.stringify({
                someNumber: 123,
                someString: "hello",
                someNumbers: [1,2,3]
            });
        }
    },
    // other stuff
});
// Startup.cs
services.RegisterDataTables(ctx =>
{
    var appJson = ctx.ValueProvider.GetValue("appJson").FirstValue ?? "{}";
    return JsonConvert.DeserializeObject<IDictionary<string, object>>(appJson);
}, true);

// DataTablesExts.cs
public static class DataTablesExts
{
    public static T Get<T>(this IDataTablesRequest request, string key)
    {
        var obj = request.AdditionalParameters[key];
        return Convert<T>(obj);
    }

    public static bool TryGet<T>(this IDataTablesRequest request, string key, out T value)
    {
        if (request.AdditionalParameters.TryGetValue(key, out object obj))
        {
            value = Convert<T>(obj);
            return true;                
        }

        value = default(T);
        return false;
    }

    private static readonly JToken JNull = JToken.Parse("null");
    private static T Convert<T>(object obj)
    {
        T AsNull() => JNull.ToObject<T>();

        if (obj == null)
            return AsNull();

        if (obj is T tobj)
            return tobj;

        if (obj is JToken jt)
            return jt.ToObject<T>();

        try
        {
            return JToken.FromObject(obj).ToObject<T>();
        }
        catch (FormatException) when (obj is string s && string.IsNullOrWhiteSpace(s))
        {
            return AsNull();
        }
    }
}

// SomeController.cs or SomePage.cshtml.cs
public async Task<DataTablesJsonResult> OnGetQueryAsync(IDataTablesRequest request)
{
    var someNumber = request.Get<int>("someNumber");
    var someString = request.Get<string>("someString");
    var someNumbers = request.Get<int[]>("someNumbers");
    // do other work here....
}

Not the best thing in the world, but damn, I just wanted to get on with building my app.

bitmoji