DevExpress / DevExtreme.AspNet.Data

DevExtreme data layer extension for ASP.NET
MIT License
154 stars 128 forks source link

Binding DataSourceLoadOptions in ASP.NET Boilerplate dynamic controllers #268

Closed AlekseyMartynov closed 6 years ago

AlekseyMartynov commented 6 years ago

Started in https://github.com/DevExpress/DevExtreme.AspNet.Data/issues/231#issuecomment-396456741


If I understand correctly, you are talking about ASP.NET Boilerplate dynamic controllers. According to their docs, model binders should work (see our docs and example). Replacing fields with properties won't make binding automatic.

When model binders don't work, you can use DataSourceLoadOptionsParser directly from an action method:

var loadOptions = new MyLoadOptions(); // a subclass of DataSourceLoadOptions
var query = Request.GetQueryNameValuePairs();
DataSourceLoadOptionsParser.Parse(loadOptions, name => query.FirstOrDefault(i => i.Key == name).Value);

or

var loadOptions = new MyLoadOptions(); // a subclass of DataSourceLoadOptions
DataSourceLoadOptionsParser.Parse(loadOptions, name => Request.QueryString[name]);

@AlekseyMartynov correct. If I make a subclass with getters/setters and then return back a DataSourceLoadOption it works... My original issue is the dynamic controller created an empty parameter list for the the DataSourceLoadOption class.

Not sure if youre familiar with the framework but the dynamic api converts app services over to webapi and js actions.

EX: You see how the 2nd method has no input variables. (If I make a subclass with getters/setters etc, it generates the parameters)

[DontWrapResult]
public virtual object GetAllDevExtremeByJsonOptions(string input, bool withIncludes = true)
{
    CheckGetPermission();

    DataSourceLoadOptions options;
    if (!string.IsNullOrEmpty(input))
    {
        options = JsonConvert.DeserializeObject<DataSourceLoadOptions>(input, new JsonSerializerSettings {ContractResolver = new DefaultContractResolver()});
    }
    else
    {
        throw new UserFriendlyException("LoadOptions is Null!", "The load options should not be empty for a DevExtreme end point!");
    }

    return GetAllDevExtreme(options, withIncludes);
}

[DontWrapResult]
public virtual object GetAllDevExtreme(DataSourceLoadOptions input, bool withIncludes = true)
{
    CheckGetPermission();

    return DataSourceLoader.Load(GetAllQueryable(withIncludes), input).ToJsonString(false, true);
}
// action 'getAllDevExtremeByJsonOptions'
abp.services.app.bRC.getAllDevExtremeByJsonOptions = function(input, withIncludes, ajaxParams) {
  return abp.ajax($.extend(true, {
    url: abp.appPath + 'api/services/app/BRC/GetAllDevExtremeByJsonOptions' + abp.utils.buildQueryString([{ name: 'input', value: input }, { name: 'withIncludes', value: withIncludes }]) + '',
    type: 'GET'
  }, ajaxParams));;
};

// action 'getAllDevExtreme'
abp.services.app.bRC.getAllDevExtreme = function(input, withIncludes, ajaxParams) {
  return abp.ajax($.extend(true, {
    url: abp.appPath + 'api/services/app/BRC/GetAllDevExtreme' + abp.utils.buildQueryString([{ name: 'withIncludes', value: withIncludes }]) + '',
    type: 'GET'
  }, ajaxParams));;
};

This is what I was trying to do: (which now works if I just pass the JSON over and convert in the method above, Is there a better way that you know of?)

var gridData = new DevExpress.data.DataSource({
    load: function(loadOptions) {
        abp.log.debug("Initial Load Options:");
        abp.log.debug(loadOptions);
        abp.log.debug(stringToPascalJSON(loadOptions));
        var deferred = $.Deferred();
        abp.services.app.bRC.getAllDevExtremeByJsonOptions(JSON.stringify(stringToPascalJSON(loadOptions)))
            .done(function(result) {
                abp.log.debug("Response");
                abp.log.debug(result);
                deferred.resolve({ data: result.data, totalCount: result.totalCount });
            });
        return deferred.promise();
    },
    byKey: function(key) {
        abp.log.debug("ByKey: " + key);
        return { id: key };
    }
});
AlekseyMartynov commented 6 years ago

@bbakermmc

bbakermmc commented 6 years ago

@AlekseyMartynov thanks for the update, some of it is useful and I can use.

This function is auto generated from the framework so I just call abp.services.app.bRC.getAllDevExtreme (my_input)

// action 'getAllDevExtreme'
abp.services.app.bRC.getAllDevExtreme = function(input, withIncludes, ajaxParams) {
  return abp.ajax($.extend(true, {
    url: abp.appPath + 'api/services/app/BRC/GetAllDevExtreme' + abp.utils.buildQueryString([{ name: 'withIncludes', value: withIncludes }]) + '',
    type: 'GET'
  }, ajaxParams));;
};

The reason for the custom class as I said is because of the dynamic controller. As you can see above the JS function wouldnt even render the input into the function so when I called it, it wouldnt even put it in the call to the server. The pascalCaseing was my testing to see if something was not rendering properly since we know on the reverse side it doesnt work.

I ended up finding the .RemoteController().LoadURL("../api/services/app/BRC/GetAllDevExtreme").LoadMethod("GET") which works for me and also saves a bunch of code in my template builder 👍 since I actually use the MVC wrappers. I didnt want to have to generate a bunch of controllers for DevExtreme just to load some data when the methods existed in the dynamic api controller, I just didnt know how to reference it properly with the .MVC format.

AlekseyMartynov commented 6 years ago

It's great to hear that you have found a good solution. Another good case for using RemoteController!

To make it even better, I can suggest to use Url.Content to deal with relative URLs:

.RemoteController().LoadUrl(Url.Content("~/api/services/..."))
bbakermmc commented 6 years ago

@AlekseyMartynov

How do I replace the work "Key" with "Id"

data: "{\"key\":18}"​
dataType: "text"​
method: "DELETE"​
url: "../api/services/app/BRC/DeleteByIdAsync"
.DataSource(ds => ds.RemoteController()
                        .LoadUrl(Url.Content("../api/services/app/BRC/GetAllDevExtreme"))
                        .DeleteUrl(Url.Content("../api/services/app/BRC/DeleteByIdAsync"))
                        .InsertUrl(Url.Content("../api/services/app/BRC/CreateOrTISP"))
                        .UpdateUrl(Url.Content("../api/services/app/BRC/UpdateOrTISP")).UpdateMethod("POST").OnBeforeSend("beforesend")
                        .Key("Id"))

//value is always 0 when pass here, even if I dont call before send.
        public virtual async Task DeleteByIdAsync(int id)
        {
            CheckDeletePermission();

            await Repository.DeleteAsync(id);
            await CurrentUnitOfWork.SaveChangesAsync();
        }
function beforesend(operation, ajaxSettings) {
            if (operation === "delete") {
                ajaxSettings.data = JSON.stringify(ajaxSettings.data);
                abp.log.debug(ajaxSettings);
            }
            if (operation === "insert") {
                ajaxSettings.data = { input: ajaxSettings.data };

        abp.log.debug(ajaxSettings);
        }

    }

Im also getting a 415 Unsupported Media Type on my insert post which is odd and im trying to figure out also if you have an idea.

AlekseyMartynov commented 6 years ago

How do I replace the work "Key" with "Id"

onBeforeSend: function(operation, ajaxSettings) {
if (operation == "delete") {
ajaxSettings.data = { id: ajaxSettings.data.key };
}
}

Im also getting a 415 Unsupported Media Type on my insert post which is odd and im trying to figure out also if you have an idea.

You probably need to add the Accept header, or experiment with dataType. From https://stackoverflow.com/q/11492325.

bbakermmc commented 6 years ago

This can be closed, I figured out my issues finally. Part of them were related to the fact that the webapi is dynamic and that its webapi and only allows for 1 parameter. So I made a method that took a jobject and parsed the data out of that rather than passing in 1 object to body and rest via querystring.