Пользовательский HtmlHelper MVC3, частичный просмотр или другое решение для применения принципа DRY

#asp.net-mvc-3 #razor

#asp.net-mvc-3 #razor

Вопрос:

У меня есть представление MVC3, доступное только для чтения, которое содержит таблицу, отображающую свойства элемента.

Для многих свойств элемента мы отслеживаем изменения, внесенные поставщиком в элемент. Так, например, поставщик может обновить свойство с именем «Цвет» со значения «Синий» на «Красный». В этом представлении в таблице перечислены все свойства, отслеживаемые в строке таблицы, со столбцом, показывающим «Старое значение» и «Новое значение». В следующем столбце либо отображается статус текущего изменения (ожидает утверждения, Одобрено или Отклонено). Однако для пользователей с правами администратора столбец будет содержать ссылки («Одобрить», «Отклонить» или «Сбросить до ожидающего утверждения»).

Мой код разметки и Razor для этого очень повторяющийся и выходит из-под контроля. Я хотел бы создать HtmlHelper для этого или, возможно, частичный просмотр, который я могу использовать для перемещения всего кода, а затем использовать его для каждого свойства элемента.

Вот пример кода, используемого для одного свойства. Этот код повторяется еще для примерно 10 свойств.

Я использую некоторые jquery и ajax для выполнения действий. Например, когда изменение отклоняется, пользователь должен ввести причину отклонения изменения.

     <tr id="rowId-color">
        <td>@Html.LabelFor(model => model.Color)</td>
        <td>@Html.DisplayFor(model => model.Color)</td>
        @if (Model.ChangeLog != null amp;amp; Model.ChangeLog.Item("Color") != null) {
            var change = Model.ChangeLog.Item("Color");
            var changeStatus = (ItemEnumerations.ItemChangeStatuses)change.ItemChangeStatusID;
            <td>@change.OldValueDisplay</td>
            <td id="tdstatusId-@change.ItemChangeID">                                                                                
                @if (changeStatus == ItemEnumerations.ItemChangeStatuses.AwaitingApproval amp;amp; User.IsInRole("TVAPMgr")) {
                                            @Ajax.ActionLink("Approve", "Approve", new { itemChangeID = change.ItemChangeID }, new AjaxOptions { HttpMethod = "POST", Confirm = "Approve this change?", OnSuccess = "actionCompleted" })
                                            @Html.Raw("|")
                                            <a href="#dialog" name="reject" data-id="@change.ItemChangeID" >Reject</a>
                }
                else if ((changeStatus == ItemEnumerations.ItemChangeStatuses.Rejected || changeStatus == ItemEnumerations.ItemChangeStatuses.Approved) amp;amp; User.IsInRole("TVAPMgr")) { 
                    @Ajax.ActionLink("Reset to Awaiting Approval", "Reset", new { itemChangeID = change.ItemChangeID }, new AjaxOptions { HttpMethod = "POST", Confirm = "Reset this change to Awaiting Approval?", OnSuccess = "actionCompleted" })
                }
                else {
                    @changeStatus.ToDisplayString()
                }
            </td> 
            <td  id="tdreasonId-@change.ItemChangeID">@Html.DisplayFor(m => m.ChangeLog.Item(change.ItemChangeID).RejectedReason)</td>
        }
        else {
            <td colspan="3">No Change</td>
        }
    </tr>
 

Ответ №1:

Это действительно больше похоже на DisplayTemplate для ItemChangeModel типа, таким образом, вы можете просто сделать:

  <tr id="rowId-color">
    <td>@Html.LabelFor(model => model.Color)</td>
    <td>@Html.DisplayFor(model => model.Color)</td>
    @Html.DisplayFor(m => m.ChangeLog.Item("Color"))
</tr>
 

Для каждой ячейки журнала изменений и шаблона отображения это похоже на мини-представление с типизированной моделью ItemChangeModel . Таким образом, ваш файл просмотра должен выглядеть следующим образом:

 @model ItemChangeModel

@if(Model != null) {

<td>@Html.DisplayFor(m => m.OldValueDisplay)</td>
<td id="tdstatusId-@Model.ItemChangeID">

@switch((ItemEnumerations.ItemChangeStatuses) Model.ItemChangeStatusID) {

    case ItemEnumerations.ItemChangeStatuses.AwaitingApproval:
        if(User.IsInRole("TVAPMgr")) {

            @Ajax.ActionLink("Approve", "Approve", new { itemChangeID = change.ItemChangeID }, new AjaxOptions { HttpMethod = "POST", Confirm = "Approve this change?", OnSuccess = "actionCompleted" })

            @Html.Raw("|")

            <a href="#dialog" name="reject" data-id="@change.ItemChangeID" >Reject</a>

        }
        break;

    case ItemEnumerations.ItemChangeStatuses.Rejected:
    case ItemEnumerations.ItemChangeStatuses.Approved:

        if(User.IsInRole("TVAPMgr")) { 
                @Ajax.ActionLink("Reset to Awaiting Approval", "Reset", new { itemChangeID = change.ItemChangeID }, new AjaxOptions { HttpMethod = "POST", Confirm = "Reset this change to Awaiting Approval?", OnSuccess = "actionCompleted" })
        } else {
                @changeStatus.ToDisplayString()
        }

        @break;
 }

  </td> 
  <td  id="tdreasonId-@change.ItemChangeID">@Html.DisplayFor(m => m.RejectedReason)          </td>
} else {
    <td colspan="3">No Change</td>
}
 

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

Вы добавляете этот шаблон отображения (с именем файла ItemChangeModel.cshtml) в папку Views Shared DisplayTemplates, и он будет использоваться всякий раз, когда для этого типа выполняется вызов DisplayFor.

В комментариях было отмечено, что вы не можете использовать метод в DisplayFor, но вы можете изменить его на индексированное свойство:

 public class ChangeLog
{
    public ItemChangeModel this[string key] { get { return Item("Color"); } }
}
 

Затем используйте:

 @Html.DisplayFor(m => m.ChangeLog["Color"])
 

Комментарии:

1. Если вы хотите стать еще более причудливым, вы можете добавить DisplayTemplate с именем ChangeHistory, который может повторять свойства и делать это также более автоматизированным способом ModelMetadata.Properties .

2. Я думаю, что ваш ответ сработает. Предложение комментария было бы затруднительным, поскольку в каждом из свойств есть некоторые различия, но в интересующем меня разделе достаточно сходств. Не могли бы вы добавить немного кода к своему ответу. Тип, возвращаемый из журнала изменений. Элемент имеет тип ItemChangeModel

3. m => m.ChangeLog.Item("Color") недопустимое выражение, которое принял бы строго типизированный помощник DisplayFor. Он содержит вызов функции, и это не поддерживается.

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

5. Дмитрий прав, вы не можете использовать метод для вызова DisplayFor, но вы можете так же легко использовать @Html.Display или изменить Item свойство, чтобы оно было индексируемым свойством, а не методом.

Ответ №2:

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

В любом случае, одним из возможных подходов является написание пользовательского помощника HTML, чтобы ваш вид выглядел следующим образом:

 <tr id="rowId-color">
    @Html.DisplayFor(x => x.Color)
    @Html.ChangeLogFor(x => x.Color)
</tr>
...
 

и помощник может быть чем-то вроде:

 public static class HtmlExtensions
{
    public static IHtmlString ChangeLogFor<TProperty>(
        this HtmlHelper<MyViewModel> html, 
        Expression<Func<MyViewModel, TProperty>> ex
    )
    {
        var model = html.ViewData.Model;
        var itemName = ((MemberExpression)ex.Body).Member.Name;
        var change = model.ChangeLog.Item(itemName);
        if (change == null)
        {
            return new HtmlString("<td colspan="3">No Change</td>");
        }

        var isUserTVAPMgr = html.ViewContext.HttpContext.User.IsInRole("TVAPMgr");
        var changeStatus = (ItemChangeStatuses)change.ItemChangeStatusID;

        var sb = new StringBuilder();
        sb.AppendFormat("<td>{0}</td>", html.Encode(change.OldValueDisplay));
        sb.AppendFormat("<td id="tdstatusId-{0}">", change.ItemChangeID);
        var ajax = new AjaxHelper<MyViewModel>(html.ViewContext, html.ViewDataContainer);
        if (changeStatus == ItemChangeStatuses.AwaitingApproval amp;amp; isUserTVAPMgr)
        {
            sb.Append(
                ajax.ActionLink(
                    "Approve", 
                    "Approve", 
                    new { 
                        itemChangeID = change.ItemChangeID 
                    }, 
                    new AjaxOptions { 
                        HttpMethod = "POST", 
                        Confirm = "Approve this change?", 
                        OnSuccess = "actionCompleted" 
                }).ToHtmlString()
            );
            sb.Append("|");
            sb.AppendFormat("<a href="#dialog" name="reject" data-id="{0}">Reject</a>", change.ItemChangeID);
        }
        else if ((changeStatus == ItemChangeStatuses.Rejected || changeStatus == ItemChangeStatuses.Approved) amp;amp; isUserTVAPMgr)
        {
            sb.Append(
                ajax.ActionLink(
                    "Reset to Awaiting Approval", 
                    "Reset", 
                    new { 
                        itemChangeID = change.ItemChangeID 
                    }, 
                    new AjaxOptions { 
                        HttpMethod = "POST", 
                        Confirm = "Reset this change to Awaiting Approval?", 
                        OnSuccess = "actionCompleted" 
                    }
                ).ToHtmlString()
            );
        }
        else
        {
            sb.Append(changeStatus.ToDisplayString());
        }

        sb.AppendLine("</td>");
        sb.AppendFormat(
            "<td id="tdreasonId-{0}">{1}</td>", 
            change.ItemChangeID, 
            html.Encode(model.ChangeLog.Item(change.ItemChangeID).RejectedReason)
        );
        return new HtmlString(sb.ToString());
    }
}
 

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

Комментарии:

1. Я использовал ваш код почти дословно, и он работает для большинства свойств. Однако отслеживание изменений использует имена свойств в качестве ключей, однако они не всегда совпадают со свойствами этой модели. Например, модель имеет свойство Material, но при изменении используется ключ MaterialID . Есть ли аннотация данных, которую я могу использовать для их сопоставления? Я думал об использовании атрибута KeyAttribute в системе. ComponentModel. Примечания к данным.

2. @JeffReddy, да, вы могли бы использовать аннотации данных для этого случая. Их значения будут считываться из метаданных модели внутри помощника.

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