Ответ 1
В связи с тем, что JsonValueProviderFactory реализовано, словарные словари привязки не поддерживаются.
Я пытаюсь сделать следующее: Модель со встроенным словарем отправляет ее по первому запросу ajax, а затем возвращает результат и снова передает его в контроллер.
Это должно проверить, что я могу вернуть словарь в моей модели. Это не работает.
Здесь мой простой тест:
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; }
}
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) {
}
});
}
});
});
В firebug, полученный json, и отправленные json идентичны. Я могу только предположить, что на пути что-то потеряется.
У кого-нибудь есть идея, что я делаю неправильно?
В связи с тем, что JsonValueProviderFactory реализовано, словарные словари привязки не поддерживаются.
Несчастливое обходное решение:
data.dictionary = {
'A': 'a',
'B': 'b'
};
data.dictionary = JSON.stringify(data.dictionary);
. . .
postJson('/mvcDictionaryTest', data, function(r) {
debugger;
}, function(a,b,c) {
debugger;
});
postJSON js lib function (использует 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
});
}
Объект ViewModel размещается (предположительно, имеет гораздо больше продолжения, чем словарь):
public class TestViewModel
{
. . .
//public Dictionary<string, string> dictionary { get; set; }
public string dictionary { get; set; }
. . .
}
Метод контроллера, отправляемый в:
[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
}
Сегодня я столкнулся с той же проблемой и придумал решение, которое не требует ничего, кроме регистрации нового связующего устройства. Это немного хаки, но, надеюсь, это помогает кому-то.
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 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;
}
}
Связывание зарегистрировано в файле Global.asax в приложении application_start
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders.Add(typeof(Dictionary<string,string>), new DictionaryModelBinder());
}
Я получил его для работы с настраиваемым связующим устройством и изменением способа отправки данных; без использования Stringify и установки типа контента.
JavaScript:
$(function() {
$.ajax({
url: '/home/a',
type: 'POST',
success: function(result) {
$.ajax({
url: '/home/a',
data: result,
type: 'POST',
success: function(result) {
}
});
}
});
});
Пользовательское связующее устройство:
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;
}
}
И добавив привязку модели в Global.asax:
ModelBinders.Binders[typeof(IDictionary<string, string>)] = new DictionaryModelBinder();
Возьмите пакет NuGet для System.Json
, который включает новый тип JsonValue
. JsonValue
- это гибкий новый тип JSON, который полностью поддерживает динамику С# 4, а также IEnumerable<KeyValuePair<string, JsonValue>>
, если вы хотите обрабатывать полезную нагрузку в качестве словаря/ассоциативного массива.
Вы можете выбрать System.Json(бета) с NuGet здесь. Кажется, что System.Json
будет включено изначально в .NET 4.5, как показано на странице здесь.
Вы также можете прочитать следующую статью, чтобы помочь правильному десериализации объектов JSON HTTP в объектах JsonValue в параметрах метода Action:
JSON, ASP.NET MVC и JQuery - Работа с Untyped JSON упрощена
Два соответствующих фрагмента кода из вышеприведенной статьи - это DynamicJsonBinder и DynamicJsonAttribute, реплицированные здесь для потомков:
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();
}
}
Соответствующим примером использования примера будет:
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);
}
}
Просто используйте лучший десериализатор. Эта первая строка, где я устанавливаю позицию, заключается в том, что JsonValueProvider оставляет поток в конце. Больше MS JSON не работает.
Request.InputStream.Position = 0;
var reader = new StreamReader(Request.InputStream);
var model = Newtonsoft.Json.JsonConvert.DeserializeObject<CreativeUploadModel>(reader.ReadToEnd());
Итак, где-то в этом объектном графе CreativeUploadModel есть такая пропозиция:
public Dictionary<string, Asset> Assets { get; set; }
который десериализуется из (например):
"assets":{"flash":{"type":"flash","value":"http://1234.cloudfront.net/1234.swf","properties":"{\"clickTag\":\"clickTAG\"}"}
Newtonsoft JSON является поставщиком JSON по умолчанию для WebAPI... поэтому он никуда не уйдет.
Вот мое решение аналогичной проблемы:
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());
}
}
}
Удачи!:-П Приветствую Łukasz Duda
Поместите сложный объект в виде строки и десериализуйте на другом конце. Однако для этого нет безопасности типов. Вот словарь со строковыми и строковыми значениями.
JS:
var data = { 'dictionary': JSON.stringify({'A': ['a', 'b'] }) };
$.ajax({
url: '/Controller/MyAction',
data: JSON.stringify(data),
type: 'POST',
contentType: 'application/json',
dataType: 'json'
});
Контроллер С#:
[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();
}
Для тех, кто недавно пришел к этой проблеме, пока вам не нужен ваш контроллер, чтобы специально принять словарь, вы можете сделать следующее:
HttpResponseMessage SomeMethod([FromBody] IEnumerable<KeyValuePair<Key, Value>> values)
{
Dictionary<Key, Value> dictionary = values.ToDictionary(x => x.Key, x = x.Value);
}
Хотя это немного хаки.
Используя ASP.NET 5 и MVC 6 прямо из коробки, я делаю это:
JSon:
{
"Name": "somename",
"D": {
"a": "b",
"b": "c",
"c": "d"
}
}
Контроллер:
[HttpPost]
public void Post([FromBody]Dictionary<string, object> dictionary)
{
}
Это то, что появляется, когда оно появляется (имя и D - это клавиши):