web-dev-qa-db-fra.com

Enregistrement de données JSON dans ASP.NET MVC

J'essaie d'obtenir une liste d'éléments de campagne sur une page Web à l'aide de JSON, qui sera ensuite manipulée et renvoyée au serveur par une requête ajax utilisant la même structure JSON que celle qui est arrivée (à l'exception des modifications apportées aux valeurs de champ). 

Recevoir des données du serveur est facile, la manipulation encore plus facile! mais envoyer ces données JSON au serveur pour économiser ... du temps de suicide! S'IL VOUS PLAÎT peut quelqu'un aider!

Javascript

var lineitems;

// get data from server
$.ajax({
    url: '/Controller/GetData/',
    success: function(data){
        lineitems = data;
    }
});

// post data to server
$.ajax({
    url: '/Controller/SaveData/',
    data: { incoming: lineitems }
});

C # - Objets

public class LineItem{
    public string reference;
    public int quantity;
    public decimal amount;
}

C # - Contrôleur

public JsonResult GetData()
{
    IEnumerable<LineItem> lineItems = ... ; // a whole bunch of line items
    return Json(lineItems);
}

public JsonResult SaveData(IEnumerable<LineItem> incoming){
    foreach(LineItem item in incoming){
        // save some stuff
    }
    return Json(new { success = true, message = "Some message" });
}

Les données arrivent sur le serveur sous forme de données de publication sérialisées. Le classeur de modèle automatisé tente de lier IEnumerable<LineItem> incoming et, de manière surprenante, la IEnumerable résultante a le nombre correct de LineItems - elle ne les remplit tout simplement pas avec des données.

SOLUTION

En utilisant les réponses d’un certain nombre de sources, principalement djch sur un autre poste de stackoverflow et BeRecursive ci-dessous, j’ai résolu mon problème en utilisant deux méthodes principales.

Du côté serveur 

Le désérialiseur ci-dessous nécessite une référence à System.Runtime.Serialization et using System.Runtime.Serialization.Json

    private T Deserialise<T>(string json)
    {
        using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(json)))
        {
            var serialiser = new DataContractJsonSerializer(typeof(T));
            return (T)serialiser.ReadObject(ms);
        }
    }

    public void Action(int id, string items){
        IEnumerable<LineItem> lineitems = Deserialise<IEnumerable<LineItem>>(items);
        // do whatever needs to be done - create, update, delete etc.
    }

Côté client

Il utilise la méthode stringify de json.org, disponible dans cette dépendance https://github.com/douglascrockford/JSON-js/blob/master/json2.js (qui est de 2,5 Ko lorsque minifiée)

        $.ajax({
            type: 'POST',
            url: '/Controller/Action',
            data: { 'items': JSON.stringify(lineItems), 'id': documentId }
        });
56
Jimbo

Jetez un coup d'œil à l'article de Phil Haack sur modèle de liaison de données JSON . Le problème est que le classeur de modèles par défaut ne sérialise pas JSON correctement. Vous avez besoin d'une sorte de ValueProvider OR pour écrire un classeur de modèle personnalisé:

using System.IO;
using System.Web.Script.Serialization;

public class JsonModelBinder : DefaultModelBinder {
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
            if(!IsJSONRequest(controllerContext)) {
                return base.BindModel(controllerContext, bindingContext);
            }

            // Get the JSON data that's been posted
            var request = controllerContext.HttpContext.Request;
            //in some setups there is something that already reads the input stream if content type = 'application/json', so seek to the begining
            request.InputStream.Seek(0, SeekOrigin.Begin);
            var jsonStringData = new StreamReader(request.InputStream).ReadToEnd();

            // Use the built-in serializer to do the work for us
            return new JavaScriptSerializer()
                .Deserialize(jsonStringData, bindingContext.ModelMetadata.ModelType);

            // -- REQUIRES .NET4
            // If you want to use the .NET4 version of this, change the target framework and uncomment the line below
            // and comment out the above return statement
            //return new JavaScriptSerializer().Deserialize(jsonStringData, bindingContext.ModelMetadata.ModelType);
        }

        private static bool IsJSONRequest(ControllerContext controllerContext) {
            var contentType = controllerContext.HttpContext.Request.ContentType;
            return contentType.Contains("application/json");
        }
    }

public static class JavaScriptSerializerExt {
        public static object Deserialize(this JavaScriptSerializer serializer, string input, Type objType) {
            var deserializerMethod = serializer.GetType().GetMethod("Deserialize", BindingFlags.NonPublic | BindingFlags.Static);

            // internal static method to do the work for us
            //Deserialize(this, input, null, this.RecursionLimit);

            return deserializerMethod.Invoke(serializer,
                new object[] { serializer, input, objType, serializer.RecursionLimit });
        }
    }

Et dites à MVC de l’utiliser dans votre fichier Global.asax:

ModelBinders.Binders.DefaultBinder = new JsonModelBinder();

En outre, ce code utilise le type de contenu = 'application/json', alors assurez-vous de le définir dans jquery comme suit:

$.ajax({
    dataType: "json",
    contentType: "application/json",            
    type: 'POST',
    url: '/Controller/Action',
    data: { 'items': JSON.stringify(lineItems), 'id': documentId }
});
29
BeRecursive

Le moyen le plus simple de le faire

Je vous invite à lire cet article de blog qui traite directement de votre problème.

L'utilisation de classeurs de modèles personnalisés n'est pas vraiment judicieuse, comme l'a souligné Phil Haack (son article sur son blog est également lié à l'article précédent).

En gros, vous avez trois options:

  1. Ecrivez une JsonValueProviderFactory et utilisez une bibliothèque côté client telle que json2.js pour communiquer directement avec JSON.

  2. Ecrivez une JQueryValueProviderFactory qui comprend la transformation d'objet JSON jQuery effectuée dans $.ajax ou

  3. Utilisez le plugin jQuery très simple et rapide décrit dans l'article du blog, qui prépare tout objet JSON (même arrays qui sera lié à IList<T> et dates qui sera correctement analysé côté serveur comme des instances DateTime cela sera compris par le classeur de modèle par défaut Asp.net MVC.

Parmi les trois, le dernier est le plus simple et n'interfère pas avec le fonctionnement interne de Asp.net MVC, réduisant ainsi la surface de bogue possible. En utilisant cette technique décrite dans l'article de blog, les données lieront correctement vos paramètres d'action de type fort et les valideront également. Il s’agit donc d’une situation win win.

12
Robert Koritnik

Dans MVC3, ils ont ajouté ceci.

Mais surtout, puisque le code source de MVC est ouvert, vous pouvez saisir le ValueProvider et l’utiliser vous-même dans votre propre code (si vous n’êtes pas encore sur MVC3).

Vous allez vous retrouver avec quelque chose comme ça

ValueProviderFactories.Factories.Add(new JsonValueProviderFactory())
8
Kenny Eliasson

J'ai résolu ce problème en suivant les conseils de vestigal ici:

Puis-je définir une longueur illimitée pour maxJsonLength dans web.config?

Lorsque je devais poster un JSON volumineux à une action dans un contrôleur, j'obtenais le fameux "Erreur lors de la désérialisation à l'aide du JSON JavaScriptSerializer. La longueur de la chaîne dépasse la valeur définie pour la propriété maxJsonLength.\R\nNom du paramètre: entrée fournisseur de valeur ".

Ce que j'ai fait est de créer un nouveau ValueProviderFactory, LargeJsonValueProviderFactory, et définir MaxJsonLength = Int32.MaxValue dans la méthode GetDeserializedObject

public sealed class LargeJsonValueProviderFactory : ValueProviderFactory
{
    private static void AddToBackingStore(LargeJsonValueProviderFactory.EntryLimitedDictionary backingStore, string prefix, object value)
    {
        IDictionary<string, object> dictionary = value as IDictionary<string, object>;
        if (dictionary != null)
        {
            foreach (KeyValuePair<string, object> keyValuePair in (IEnumerable<KeyValuePair<string, object>>) dictionary)
                LargeJsonValueProviderFactory.AddToBackingStore(backingStore, LargeJsonValueProviderFactory.MakePropertyKey(prefix, keyValuePair.Key), keyValuePair.Value);
        }
        else
        {
            IList list = value as IList;
            if (list != null)
            {
                for (int index = 0; index < list.Count; ++index)
                    LargeJsonValueProviderFactory.AddToBackingStore(backingStore, LargeJsonValueProviderFactory.MakeArrayKey(prefix, index), list[index]);
            }
            else
                backingStore.Add(prefix, value);
        }
    }

    private static object GetDeserializedObject(ControllerContext controllerContext)
    {
        if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
            return (object) null;
        string end = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd();
        if (string.IsNullOrEmpty(end))
            return (object) null;

        var serializer = new JavaScriptSerializer {MaxJsonLength = Int32.MaxValue};

        return serializer.DeserializeObject(end);
    }

    /// <summary>Returns a JSON value-provider object for the specified controller context.</summary>
    /// <returns>A JSON value-provider object for the specified controller context.</returns>
    /// <param name="controllerContext">The controller context.</param>
    public override IValueProvider GetValueProvider(ControllerContext controllerContext)
    {
        if (controllerContext == null)
            throw new ArgumentNullException("controllerContext");
        object deserializedObject = LargeJsonValueProviderFactory.GetDeserializedObject(controllerContext);
        if (deserializedObject == null)
            return (IValueProvider) null;
        Dictionary<string, object> dictionary = new Dictionary<string, object>((IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase);
        LargeJsonValueProviderFactory.AddToBackingStore(new LargeJsonValueProviderFactory.EntryLimitedDictionary((IDictionary<string, object>) dictionary), string.Empty, deserializedObject);
        return (IValueProvider) new DictionaryValueProvider<object>((IDictionary<string, object>) dictionary, CultureInfo.CurrentCulture);
    }

    private static string MakeArrayKey(string prefix, int index)
    {
        return prefix + "[" + index.ToString((IFormatProvider) CultureInfo.InvariantCulture) + "]";
    }

    private static string MakePropertyKey(string prefix, string propertyName)
    {
        if (!string.IsNullOrEmpty(prefix))
            return prefix + "." + propertyName;
        return propertyName;
    }

    private class EntryLimitedDictionary
    {
        private static int _maximumDepth = LargeJsonValueProviderFactory.EntryLimitedDictionary.GetMaximumDepth();
        private readonly IDictionary<string, object> _innerDictionary;
        private int _itemCount;

        public EntryLimitedDictionary(IDictionary<string, object> innerDictionary)
        {
            this._innerDictionary = innerDictionary;
        }

        public void Add(string key, object value)
        {
            if (++this._itemCount > LargeJsonValueProviderFactory.EntryLimitedDictionary._maximumDepth)
                throw new InvalidOperationException("JsonValueProviderFactory_RequestTooLarge");
            this._innerDictionary.Add(key, value);
        }

        private static int GetMaximumDepth()
        {
            NameValueCollection appSettings = ConfigurationManager.AppSettings;
            if (appSettings != null)
            {
                string[] values = appSettings.GetValues("aspnet:MaxJsonDeserializerMembers");
                int result;
                if (values != null && values.Length > 0 && int.TryParse(values[0], out result))
                    return result;
            }
            return 1000;
        }
    }
}

Ensuite, dans la méthode Application_Start de Global.asax.cs, remplacez ValueProviderFactory par le nouveau:

protected void Application_Start()
    {
        ...

        //Add LargeJsonValueProviderFactory
        ValueProviderFactory jsonFactory = null;
        foreach (var factory in ValueProviderFactories.Factories)
        {
            if (factory.GetType().FullName == "System.Web.Mvc.JsonValueProviderFactory")
            {
                jsonFactory = factory;
                break;
            }
        }

        if (jsonFactory != null)
        {
            ValueProviderFactories.Factories.Remove(jsonFactory);
        }

        var largeJsonValueProviderFactory = new LargeJsonValueProviderFactory();
        ValueProviderFactories.Factories.Add(largeJsonValueProviderFactory);
    }
3
MFA

Vous pouvez essayer ces . 1. stringify votre objet JSON avant d'appeler l'action serveur via ajax 2. désérialisez la chaîne dans l'action, puis utilisez les données comme dictionnaire.

Exemple Javascript ci-dessous (envoi de l'objet JSON

$.ajax(
   {
       type: 'POST',
       url: 'TheAction',
       data: { 'data': JSON.stringify(theJSONObject) 
   }
})

Action (C #) exemple ci-dessous

[HttpPost]
public JsonResult TheAction(string data) {

       string _jsonObject = data.Replace(@"\", string.Empty);
       var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();           
        Dictionary<string, string> jsonObject = serializer.Deserialize<Dictionary<string, string>>(_jsonObject);


        return Json(new object{status = true});

    }
2

La réponse de BeRecursive est celle que j’ai utilisée pour pouvoir utiliser Json.Net (nous avons MVC5 et WebApi 5 - WebApi 5 utilise déjà Json.Net), mais j’ai trouvé un problème. Lorsque vous avez des paramètres sur votre route sur lesquels vous postez, MVC essaie d'appeler le classeur de modèle pour les valeurs d'URI et ce code essaiera de lier le JSON publié à ces valeurs.

Exemple:

[HttpPost]
[Route("Customer/{customerId:int}/Vehicle/{vehicleId:int}/Policy/Create"]
public async Task<JsonNetResult> Create(int customerId, int vehicleId, PolicyRequest policyRequest)

La fonction BindModel est appelée trois fois, en bombardant la première, car elle tente de lier le JSON à customerId avec l’erreur: Error reading integer. Unexpected token: StartObject. Path '', line 1, position 1.

J'ai ajouté ce bloc de code en haut de BindModel:

if (bindingContext.ValueProvider.GetValue(bindingContext.ModelName) != null) {
    return base.BindModel(controllerContext, bindingContext);
}

Heureusement, ValueProvider a déterminé les valeurs de la route au moment où cette méthode est utilisée.

0
YakkoWarner

Si vous avez des données JSON entrant sous forme de chaîne (par exemple, '[{"id": 1, "name": "Charles"}, {"id": 8, "name": "John"}, { "id": 13, "name": "Sally"}] ')

Ensuite, j'utilisais JSON.net et j'utilisais Linq to JSON pour obtenir les valeurs ...

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{

    if (Request["items"] != null)
    {
        var items = Request["items"].ToString(); // Get the JSON string
        JArray o = JArray.Parse(items); // It is an array so parse into a JArray
        var a = o.SelectToken("[0].name").ToString(); // Get the name value of the 1st object in the array
        // a == "Charles"
    }
}
}
0
David Henderson

J'ai résolu en utilisant une désérialisation "manuelle" . Je vais expliquer dans le code

public ActionResult MyMethod([System.Web.Http.FromBody] MyModel model)
{
 if (module.Fields == null && !string.IsNullOrEmpty(Request.Form["fields"]))
 {
   model.Fields = JsonConvert.DeserializeObject<MyFieldModel[]>(Request.Form["fields"]);
 }
 //... more code
}
0
Antonio Santise