2011-01-17 20 views
20

Estoy intentando lo siguiente: Un modelo con un diccionario dentro lo envía en la primera solicitud de ajax y luego toma el resultado serializarlo de nuevo y enviarlo de vuelta al controlador.POST json dictionary

Esto debería probar que puedo recuperar un diccionario en mi modelo. No funciona

Aquí está mi prueba sencilla:

public class HomeController : Controller 
{ 
    public ActionResult Index (T a) 
    { 
     return View(); 
    } 

    public JsonResult A(T t) 
    { 
     if (t.Name.IsEmpty()) 
     { 
     t = new T(); 
     t.Name = "myname"; 
     t.D = new Dictionary<string, string>(); 
     t.D.Add("a", "a"); 
     t.D.Add("b", "b"); 
     t.D.Add("c", "c"); 
     } 
     return Json(t); 
    } 
} 

//model 
public class T 
{ 
    public string Name { get; set; } 
    public IDictionary<string,string> D { get; set; } 
} 

El javascript:

$(function() { 
    var o = { 
     Name: 'somename', 
     "D": { 
      "a": "b", 
      "b": "c", 
      "c": "d" 
     } 
    }; 

    $.ajax({ 
     url: actionUrl('/home/a'), 
     contentType: 'application/json', 
     type: 'POST', 
     success: function (result) { 
      $.ajax({ 
       url: actionUrl('/home/a'), 
       data: JSON.stringify(result), 
       contentType: 'application/json', 
       type: 'POST', 
       success: function (result) { 
       } 
      }); 
     } 
    }); 
}); 

En el firebug JSON JSON recibida y el enviado son idénticos. Solo puedo suponer que algo se pierde en el camino.

¿Alguien tiene una idea de lo que estoy haciendo mal?

+1

Posible duplicado de [Publicación de datos JSON en ASP.NET MVC] (http://stackoverflow.com/questions/4164114/posting-json-data-to-asp-net-mvc) –

Respuesta

13

Debido a la forma en que se implementó JsonValueProviderFactory, no es posible enlazar diccionarios.

+0

Cuidar para obtener más información sobre eso ? Quiero decir que solo lee el flujo de entrada y lo pasa al JavascriptSerializer. ¿Hace algo más raro? – sirrocco

+2

@sirrocco, hace más que esto. Mire el 'JsonValueProviderFactory' con reflector. Verás que utiliza el método 'DeserializeObject' en lugar de' Deserialize' porque en ese momento no conoce el tipo del modelo. Luego construye un 'DictionaryValueProvider' completamente nuevo y, como puede ver, solo se implementan las funciones privadas' MakePropertyKey' y 'MakeArrayKey' que generan la notación' prefix.propertyName' y 'prefix [index]'. No hay nada que maneje el caso de un diccionario que debe ser de la forma 'prefix [index] .Key' y' prefix [index] .Value'. –

+1

Así que piense en ello como un error o una característica no implementada. Como prefiera :-) –

20

Una solución desafortunada:

data.dictionary = { 
    'A': 'a', 
    'B': 'b' 
}; 

data.dictionary = JSON.stringify(data.dictionary); 

. . . 

postJson('/mvcDictionaryTest', data, function(r) { 
    debugger; 
}, function(a,b,c) { 
    debugger; 
}); 

función lib postJSON js (utiliza jQuery):

function postJson(url, data, success, error) { 
    $.ajax({ 
     url: url, 
     data: JSON.stringify(data), 
     type: 'POST', 
     contentType: 'application/json; charset=utf-8', 
     dataType: 'json', 
     success: success, 
     error: error 
    }); 
} 

El objeto modelo de vista que se ha escrito (presumiblemente tiene mucho más en juego que un diccionario):

public class TestViewModel 
{ 
    . . . 
    //public Dictionary<string, string> dictionary { get; set; } 
    public string dictionary { get; set; } 
    . . . 
} 

El método del controlador se publicó en:

[HttpPost] 
public ActionResult Index(TestViewModel model) 
{ 
    var ser = new System.Web.Script.Serialization.JavascriptSerializer(); 
    Dictionary<string, string> dictionary = ser.Deserialize<Dictionary<string, string>>(model.dictionary); 

    // Do something with the dictionary 
} 
2

Lo tengo que trabajar con un encuadernador de modelo personalizado, y cambiar la forma en que se envían los datos; sin usar Stringify y estableciendo el tipo de contenido.

JavaScript: Modelo

$(function() { 
     $.ajax({ 
      url: '/home/a', 
      type: 'POST', 
      success: function(result) { 
       $.ajax({ 
        url: '/home/a', 
        data: result, 
        type: 'POST', 
        success: function(result) { 

        } 
       }); 
      } 
     }); 
    }); 

Carpeta de encargo:

public class DictionaryModelBinder : IModelBinder 
{   
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) 
    { 
     if (bindingContext == null) 
      throw new ArgumentNullException("bindingContext"); 

     string modelName = bindingContext.ModelName; 
     IDictionary<string, string> formDictionary = new Dictionary<string, string>(); 

     Regex dictionaryRegex = new Regex(modelName + @"\[(?<key>.+?)\]", RegexOptions.CultureInvariant); 
     foreach (var key in controllerContext.HttpContext.Request.Form.AllKeys.Where(k => k.StartsWith(modelName + "["))) 
     { 
      Match m = dictionaryRegex.Match(key); 
      if (m.Success) 
      { 
       formDictionary[m.Groups["key"].Value] = controllerContext.HttpContext.Request.Form[key]; 
      } 
     } 
     return formDictionary; 
    } 
} 

y añadiendo el aglutinante de modelo en el Global.asax:

ModelBinders.Binders[typeof(IDictionary<string, string>)] = new DictionaryModelBinder(); 
1

sólo tiene que utilizar una mejor deserializer. Esa primera línea donde establezco la posición es porque JsonValueProvider deja la secuencia al final. Más MS JSON falla.

Request.InputStream.Position = 0; 
var reader = new StreamReader(Request.InputStream); 

var model = Newtonsoft.Json.JsonConvert.DeserializeObject<CreativeUploadModel>(reader.ReadToEnd()); 

Así que alguna parte de ese gráfico de objetos CreativeUploadModel hay un puntal de esta manera:

public Dictionary<string, Asset> Assets { get; set; } 

Qué está deserialized a partir de (por ejemplo):

"assets":{"flash":{"type":"flash","value":"http://1234.cloudfront.net/1234.swf","properties":"{\"clickTag\":\"clickTAG\"}"} 

Newtonsoft JSON es el valor predeterminado JSON proveedor de WebAPI ... por lo que no va a ninguna parte.

2

Obtenga el siguiente paquete NuGet para System.Json, que incluye el nuevo tipo JsonValue. JsonValue es un nuevo tipo de representante JSON flexible que admite completamente el dinámico C# 4, y también es IEnumerable<KeyValuePair<string, JsonValue>> en caso de que desee tratar una carga útil como una matriz asociativa/de diccionario.

Puede recoger System.Json (Beta) with NuGet here. Parece que System.Json se incluirá de forma nativa en .NET 4.5, como lo indica el documentation pages here.

También puede ser que desee leer el siguiente artículo para ayudarle a obtener cuerpos JSON HTTP deserializar correctamente en objetos JsonValue en sus parámetros de método de acción:

JSON, ASP.NET MVC and JQuery - Working with Untyped JSON made easy

Las dos piezas pertinentes del Código del artículo anterior sería la DynamicJsonBinder y DynamicJsonAttribute, reproducido aquí para la posteridad:

public class DynamicJsonBinder : IModelBinder 
{ 
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) 
    { 
     if (!controllerContext.HttpContext.Request.ContentType.StartsWith 
       ("application/json", StringComparison.OrdinalIgnoreCase)) 
     { 
      // not JSON request 
      return null; 
     } 

     var inpStream = controllerContext.HttpContext.Request.InputStream; 
     inpStream.Seek(0, SeekOrigin.Begin); 

     StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream); 
     string bodyText = reader.ReadToEnd(); 
     reader.Close(); 


     if (String.IsNullOrEmpty(bodyText)) 
     { 
      // no JSON data 
      return null; 
     } 

     return JsonValue.Parse(bodyText); 
    } 
} 

public class DynamicJsonAttribute : CustomModelBinderAttribute 
{ 
    public override IModelBinder GetBinder() 
    { 
     return new DynamicJsonBinder(); 
    } 
} 

Un caso relevante uso de la muestra sería:

public class HomeController : Controller 
{ 
    public ActionResult Index (T a) 
    { 
     return View(); 
    } 

    public JsonResult A([DynamicJson] JsonValue value) 
    { 
     dynamic t = value.AsDynamic(); 

     if (t.Name.IsEmpty()) 
     { 
     t = new // t is dynamic, so I figure just create the structure you need directly 
     { 
      Name = "myname", 
      D = new // Associative array notation (woot!): 
      { 
       a = "a", 
       b = "b", 
       c = "c" 
      } 
     }; 
     } 

     return Json(t); 
    } 
} 
4

Me encontré con el mismo problema hoy y se me ocurrió una solución que no requiere nada más que el registro de un nuevo modelo de carpeta. Es un poco hacky pero espero que ayude a alguien.

public class DictionaryModelBinder : IModelBinder 
    { 
     /// <summary> 
     /// Binds the model to a value by using the specified controller context and binding context. 
     /// </summary> 
     /// <returns> 
     /// The bound value. 
     /// </returns> 
     /// <param name="controllerContext">The controller context.</param><param name="bindingContext">The binding context.</param> 
     public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) 
     { 
      if (bindingContext == null) 
       throw new ArgumentNullException("bindingContext"); 

      string modelName = bindingContext.ModelName; 
      // Create a dictionary to hold the results 
      IDictionary<string, string> result = new Dictionary<string, string>(); 

      // The ValueProvider property is of type IValueProvider, but it typically holds an object of type ValueProviderCollect 
      // which is a collection of all the registered value providers. 
      var providers = bindingContext.ValueProvider as ValueProviderCollection; 
      if (providers != null) 
      { 
       // The DictionaryValueProvider is the once which contains the json values; unfortunately the ChildActionValueProvider and 
       // RouteDataValueProvider extend DictionaryValueProvider too, so we have to get the provider which contains the 
       // modelName as a key. 
       var dictionaryValueProvider = providers 
        .OfType<DictionaryValueProvider<object>>() 
        .FirstOrDefault(vp => vp.ContainsPrefix(modelName)); 
       if (dictionaryValueProvider != null) 
       { 
        // There's no public property for getting the collection of keys in a value provider. There is however 
        // a private field we can access with a bit of reflection. 
        var prefixsFieldInfo = dictionaryValueProvider.GetType().GetField("_prefixes", 
                         BindingFlags.Instance | 
                         BindingFlags.NonPublic); 
        if (prefixsFieldInfo != null) 
        { 
         var prefixes = prefixsFieldInfo.GetValue(dictionaryValueProvider) as HashSet<string>; 
         if (prefixes != null) 
         { 
          // Find all the keys which start with the model name. If the model name is model.DictionaryProperty; 
          // the keys we're looking for are model.DictionaryProperty.KeyName. 
          var keys = prefixes.Where(p => p.StartsWith(modelName + ".")); 
          foreach (var key in keys) 
          { 
           // With each key, we can extract the value from the value provider. When adding to the dictionary we want to strip 
           // out the modelName prefix. (+1 for the extra '.') 
           result.Add(key.Substring(modelName.Length + 1), bindingContext.ValueProvider.GetValue(key).AttemptedValue); 
          } 
          return result; 
         } 
        } 
       } 
      } 
      return null; 
     } 
    } 

El aglutinante se ha registrado en el archivo Global.asax bajo Application_Start

protected void Application_Start() 
    { 
     AreaRegistration.RegisterAllAreas(); 

     RegisterGlobalFilters(GlobalFilters.Filters); 
     RegisterRoutes(RouteTable.Routes); 

     ModelBinders.Binders.Add(typeof(Dictionary<string,string>), new DictionaryModelBinder()); 
    } 
+0

Muchas gracias por esta solución. Esto funcionó para mí cuando las otras soluciones publicadas aquí no. – Andy

+0

Usé este código más o menos al pie de la letra (necesitaba un diccionario ) y funcionó como un amuleto. –

1

Aquí está mi solución al problema similar:

using System.Collections.Generic; 
using System.IO; 
using System.Web.Mvc; 
using System.Web.Script.Serialization; 

namespace Controllers 
{ 
    public class DictionaryModelBinder : IModelBinder 
    { 
     public object BindModel(ControllerContext context, ModelBindingContext bindingContext) 
     { 
      context.HttpContext.Request.InputStream.Seek(0, SeekOrigin.Begin); 
      using (TextReader reader = new StreamReader(context.HttpContext.Request.InputStream)) 
      { 
       string requestContent = reader.ReadToEnd(); 
       var arguments = new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(requestContent); 
       return arguments[bindingContext.ModelName]; 
      } 
     } 
    } 
} 

using Controllers; 
using Moq; 
using NUnit.Framework; 
using System.Collections; 
using System.Collections.Generic; 
using System.IO; 
using System.Text; 
using System.Web; 
using System.Web.Mvc; 

namespace ControllersTest 
{ 
    [TestFixture] 
    public class DictionaryModelBinderTest 
    { 
     private ControllerContext controllerContext; 

     [Test] 
     public void ReturnsDeserializedPrimitiveObjectsAndDictionaries() 
     { 
      string input = 
@"{ 
    arguments: { 
     simple: 1, 
     complex: { a: 2, b: 3 }, 
     arrayOfSimple: [{ a: 4, b: 5 }], 
     arrayOfComplex: [{ a: 6, b: 7 }, { a: 8, b: 9 }]}, 
    otherArgument: 1 
}"; 
      SetUpRequestContent(input); 

      var binder = new DictionaryModelBinder(); 
      var bindingContext = new ModelBindingContext(); 
      bindingContext.ModelName = "arguments"; 

      var model = (Dictionary<string, object>)binder.BindModel(controllerContext, bindingContext); 

      Assert.IsFalse(model.ContainsKey("otherArgument")); 
      Assert.AreEqual(1, model["simple"]); 
      var complex = (Dictionary<string, object>)model["complex"]; 
      Assert.AreEqual(2, complex["a"]); 
      Assert.AreEqual(3, complex["b"]); 
      var arrayOfSimple = (ArrayList)model["arrayOfSimple"]; 
      Assert.AreEqual(4, ((Dictionary<string, object>)arrayOfSimple[0])["a"]); 
      Assert.AreEqual(5, ((Dictionary<string, object>)arrayOfSimple[0])["b"]); 
      var arrayOfComplex = (ArrayList)model["arrayOfComplex"]; 
      var complex1 = (Dictionary<string, object>)arrayOfComplex[0]; 
      var complex2 = (Dictionary<string, object>)arrayOfComplex[1]; 
      Assert.AreEqual(6, complex1["a"]); 
      Assert.AreEqual(7, complex1["b"]); 
      Assert.AreEqual(8, complex2["a"]); 
      Assert.AreEqual(9, complex2["b"]); 
     } 

     private void SetUpRequestContent(string input) 
     { 
      var stream = new MemoryStream(Encoding.UTF8.GetBytes(input)); 
      stream.Seek(0, SeekOrigin.End); 

      var controllerContextStub = new Mock<ControllerContext>(); 
      var httpContext = new Mock<HttpContextBase>(); 
      httpContext.Setup(x => x.Request.InputStream).Returns(stream); 
      controllerContextStub.Setup(x => x.HttpContext).Returns(httpContext.Object); 
      this.controllerContext = controllerContextStub.Object; 
     } 
    } 
} 

using Controllers; 
using PortalApi.App_Start; 
using System.Collections.Generic; 
using System.Web.Http; 
using System.Web.Mvc; 
using System.Web.Routing; 

namespace PortalApi 
{ 
    public class MvcApplication : System.Web.HttpApplication 
    { 
     protected void Application_Start() 
     { 
      AreaRegistration.RegisterAllAreas(); 

      WebApiConfig.Register(GlobalConfiguration.Configuration); 
      RouteConfig.RegisterRoutes(RouteTable.Routes); 
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); 
      ModelBinders.Binders.Add(typeof(Dictionary<string, object>), new DictionaryModelBinder()); 
     } 
    } 
} 

Que se diviertan! :-P Saludos Łukasz Duda

1

Coloque el objeto complejo como una cadena y deserialice en el otro extremo. Sin embargo, no hay seguridad de tipo para esto. Aquí hay un diccionario con clave de cadena y valores de matriz de cadena.

JS:

var data = { 'dictionary': JSON.stringify({'A': ['a', 'b'] }) }; 

$.ajax({ 
    url: '/Controller/MyAction', 
    data: JSON.stringify(data), 
    type: 'POST', 
    contentType: 'application/json', 
    dataType: 'json' 
}); 

C# controlador:

[HttpPost] 
public ActionResult MyAction(string dictionary) 
{ 
    var s = new System.Web.Script.Serialization.JavaScriptSerializer(); 
    Dictionary<string, string[]> d = s.Deserialize<Dictionary<string, string[]>>(dictionary); 
    return View(); 
} 
0

Uso de ASP.NET MVC 5 y 6 directamente de la caja que esté haciendo esto:

JSON:

{ 
    "Name": "somename", 
    "D": { 
     "a": "b", 
     "b": "c", 
     "c": "d" 
    } 
} 

controlador:

[HttpPost] 
public void Post([FromBody]Dictionary<string, object> dictionary) 
{ 
} 

Esto es lo que aparece cuando se trata a través de (Nombre y D son las claves):

enter image description here

1

Para cualquier persona que está por venir en este problema recientemente todavía, siempre y cuando no se necesita el controlador para aceptar específicamente un diccionario, puede hacer lo siguiente:

HttpResponseMessage SomeMethod([FromBody] IEnumerable<KeyValuePair<Key, Value>> values) 
{ 
    Dictionary<Key, Value> dictionary = values.ToDictionary(x => x.Key, x = x.Value); 
} 

A pesar de que es un poco hacky.

Cuestiones relacionadas