Модель MVC, связывающая сложный тип с простым типом и наоборот - PullRequest
2 голосов
/ 02 февраля 2011

Вот сценарий:

У меня есть плагин автозаполнения (пользовательский), который хранит скрытое поле объектов JSON (используя определенную структуру).

Я создал помощник Html, который помогает мне легко привязываться к определенной пользовательской модели (в основном, он имеет свойство JSON для двустороннего связывания и свойство, которое позволяет десериализовать JSON в соответствующую структуру):

public class AutoCompleteModel {
    public string JSON { get; set; }
    public IEnumerable<Person> People {
        get {
            return new JavaScriptSerializer().Deserialize<Person>(this.JSON);
        }
        set {
            this.JSON = new JavaScriptSerializer().Serialize(value);
        }
     }
 }

Это прекрасно работает, и я могу смоделировать связывание, используя связыватель по умолчанию @Html.Autocomplete(viewModel => viewModel.AutoCompleteModelTest). Помощник HTML генерирует HTML как:

<input type="text" id="AutoCompleteModelTest_ac" name="AutoCompleteModelTest_ac" value="" />
<input type="hidden" id="AutoCompleteModelTest_JSON" name="AutoCompleteModelTest.JSON" value="{JSON}" />

Проблема в том, что это не лучший способ для потребителей. Они должны вручную установить свойство People для массива структур Person. В моем слое данных мои доменные объекты, вероятно, не будут хранить полную структуру, только идентификатор человека (корпоративный идентификатор). Автозаполнение позаботится о поиске самого человека, только если ему будет предоставлено удостоверение личности.

Лучшим сценарием будет назвать его так:

@Html.Autocomplete(domainObject => domainObject.PersonID) или @Html.Autocomplete(domainObject => domainObject.ListOfPersonIDs

Я бы хотел, чтобы он работал против свойства строки И против пользовательской модели AutoCompleteModel. Автозаполнение обновляет только одно скрытое поле, и это имя поля возвращается при обратной передаче (значение выглядит следующим образом: [{ "Id":"12345", "FullName":"A Name"},{ "Id":"12347", "FullName":"Another Name" }]).

Проблема, конечно, в том, что эти свойства объекта домена имеют только идентификатор или массив идентификаторов, а не полную структуру Person (поэтому нельзя напрямую сериализовать в JSON). В помощнике HTML я могу преобразовать эти значения свойств в структуру, но я не знаю, как преобразовать их обратно в простой тип на POST. Решение, которое мне нужно, - преобразовать идентификатор в новую структуру Person при загрузке страницы, сериализовав его в скрытое поле. На POST он десериализует сгенерированный JSON обратно в простой массив идентификаторов.

Является ли пользовательская модель переплетом нужным мне решением? Как мне сказать, чтобы он работал как с пользовательской моделью, так и с простыми типами (потому что я не хочу, чтобы она применялась к КАЖДОМУ строковому свойству, просто нужно, чтобы он работал со значениями, заданными помощником HTML).

1 Ответ

2 голосов
/ 08 февраля 2011

Я понял, это возможно!

Чтобы уточнить, мне нужно было: преобразовать строку или массив строк (из идентификаторов) в структуру JSON для значения моего скрытого поля, затем при обратной отправке десериализовать JSON в скрытом поле и преобразовать структуру обратно в простое строка или строковый массив (из идентификаторов) для свойства объекта моего домена.

Шаг 1. Создание помощника HTML

Я уже сделал это, но только для того, чтобы принять мой собственный тип AutoCompleteModel. Мне нужен был один для строки и Enumerable типа string.

Все, что я сделал, это сгенерировал мои структуры Person из значения свойства и сериализовал их в JSON для скрытого поля, используемого Autocompleter (это пример помощника string, у меня также есть почти идентичный один для IEnumerable<string>):

public static MvcHtmlString AutoComplete<TModel>(
    this HtmlHelper<TModel> htmlHelper,
    Expression<Func<TModel, string>> idProp)
    where TModel : class
{
    TModel model = htmlHelper.ViewData.Model;
    string id = idProp.Compile().Invoke(model);

    string propertyName = idProp.GetPropertyName();

    Person[] people = new Person[] {
        new Person() { ID = id }
    };

    // Don't name the textbox the same name as the property,
    // otherwise the value will be whatever the textbox is,
    // if you care.
    MvcHtmlString textBox = htmlHelper.TextBox(propertyName + "_ac", string.Empty);

    // For me, the JSON is the value I want to postback
    MvcHtmlString hidden = htmlHelper.Hidden(propertyName, new JavaScriptSerializer().Serialize(people));

    return MvcHtmlString.Create(
        "<span class=\"AutoComplete\">" +
            textBox.ToHtmlString() +
            hidden.ToHtmlString() +
        "</span>");
}

Использование: @Html.AutoComplete(model => model.ID)

Шаг 2. Создание пользовательского связующего для модели

Суть моей проблемы заключалась в том, что мне нужно, чтобы это связыватель применялось только к определенным свойствам, а это были строки или массивы строк.

Я был вдохновлен этой статьей , потому что она использовала Generics. Я решил, эй, мы можем просто спросить людей, на какую недвижимость они хотят подать заявку на переплет.

public class AutoCompleteBinder<T> : DefaultModelBinder
    where T : class
{
    private IEnumerable<string> PropertyNames { get; set; }

    public AutoCompleteBinder(params Expression<Func<T, object>>[] idProperties)
    {
        this.PropertyNames = idProperties.Select(x => x.GetPropertyName());
    }

    protected override object GetPropertyValue(
        ControllerContext controllerContext, 
        ModelBindingContext bindingContext,
        PropertyDescriptor propertyDescriptor, 
        IModelBinder propertyBinder)
    {
        var submittedValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        if (submittedValue != null && this.PropertyNames.Contains(propertyDescriptor.Name))
        {
            string json = submittedValue.AttemptedValue;

            Person[] people = new JavaScriptSerializer().Deserialize<Person[]>(json);

            if (people != null && people.Any())
            {
                string[] IDs = people.Where(x => !string.IsNullOrEmpty(x.ID)).Select(x => x.ID).ToArray();

                bool isArray = bindingContext.ModelType != typeof(string) && 
                    (bindingContext.ModelType == typeof(string[]) || 
                    bindingContext.ModelType.HasInterface<IEnumerable>());

                if (IDs.Count() == 1 && !isArray)
                    return IDs.First(); // return string
                else if (IDs.Count() > 0 && isArray)
                    return IDs.ToArray(); // return string[]
                else
                    return null;
            }
            else
            {
                return null;
            }
        }

        return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
    }
}

GetPropertyName() (перевести выражение LINQ в строку, т.е. m => m.ID = ID) и HasInterface() - это всего лишь два служебных метода, которые у меня есть.

Шаг 3: Регистрация

Зарегистрируйте подшивку объектов вашего домена и их свойства в Application_Start:

ModelBinders.Binders.Add(typeof(Employee), new AutoCompleteBinder<Employee>(e => e.ID, e => e.TeamIDs));

Просто немного раздражает необходимость регистрировать подшивку для определенных свойств, но это не конец света и обеспечивает приятный, плавный опыт работы с моим автозаполнением.

Любые комментарии приветствуются.

...