Подтвердить что ты не робот

Определить модель частичного представления с контроллера в MVC

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

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

Я создаю нечто вроде страницы Google iGoogle. Главная страница с несколькими виджетами, которые могут перемещаться или настраиваться по мере необходимости. Текущая система загружает фактические данные виджетов, асинхронно просматривая POST на контроллер в моем приложении. Этот контроллер будет либо отображать частичный вид HTML, который может быть возвращен (а затем загружен в представление страницы JQUERY), либо просто прямой HTML/JavaScript, который хранится в базе данных.

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

Я понимаю, что это нечетный сценарий для MVC (слои смешиваются...), и любые советы по фундаментальному дизайну, или реализация этого, будут очень благодарны.

В настоящее время я использую MVC3/Razor. Не стесняйтесь задавать любые другие вопросы.

4b9b3361

Ответ 1

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

Модели

Во-первых, модели. Я решил создать два "виджета", один для новостей и один для часов.

public class NewsModel
{
    public string[] Headlines { get; set; }

    public NewsModel(params string[] headlines)
    {
        Headlines = headlines;
    }
}

public class ClockModel
{
    public DateTime Now { get; set; }

    public ClockModel(DateTime now)
    {
        Now = now;
    }
}

контроллер

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

public ActionResult Show(string widgetName)
{
    var selector = new ModelSelector();
    selector.WhenRendering<ClockModel>(() => new ClockModel(DateTime.Now));
    selector.WhenRendering<NewsModel>(() => new NewsModel("Headline 1", "Headline 2", "Headline 3"));
    return PartialView(widgetName, selector);
}

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

ModelSelector

ModelSelector, который использует контроллер, довольно прост - он просто держит пакет делегатов для создания каждого типа модели:

public class ModelSelector
{
    private readonly Dictionary<Type, Func<object>> modelLookup = new Dictionary<Type, Func<object>>();

    public void WhenRendering<T>(Func<object> getter)
    {
        modelLookup.Add(typeof(T), getter);
    }

    public object GetModel(Type modelType)
    {
        if (!modelLookup.ContainsKey(modelType))
        {
            throw new KeyNotFoundException(string.Format("A provider for the model type '{0}' was not provided", modelType.FullName));
        }

        return modelLookup[modelType]();
    }
}

Представления - простое решение

Теперь самый простой способ реализовать представление:

@model MvcApplication2.ModelSelector
@using MvcApplication2.Models
@{
    var clock = (ClockModel) Model.GetModel(typeof (ClockModel));
}

<h2>The time is: @clock.Now</h2>

Вы можете закончить здесь и использовать этот подход.

Представления - лучшее решение

Это довольно уродливо. Я хотел, чтобы мои взгляды выглядели так:

@model MvcApplication2.Models.ClockModel
<h2>Clock</h2>
@Model.Now

и

@model MvcApplication2.Models.NewsModel
<h2>News Widget</h2>
@foreach (var headline in Model.Headlines)
{
    <h3>@headline</h3>
}

Чтобы сделать эту работу, мне пришлось создать настраиваемый механизм просмотра.

Пользовательский механизм просмотра

Когда скомпилировано представление Razor, он наследует ViewPage<T>, где T - @model. Таким образом, мы можем использовать рефлексию, чтобы выяснить, какого типа вид нужен, и выбрать его.

public class ModelSelectorEnabledRazorViewEngine : RazorViewEngine
{
    protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
    {
        var result = base.CreateView(controllerContext, viewPath, masterPath);

        if (result == null)
            return null;

        return new CustomRazorView((RazorView) result);
    }

    protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
    {
        var result = base.CreatePartialView(controllerContext, partialPath);

        if (result == null)
            return null;

        return new CustomRazorView((RazorView)result);
    }

    public class CustomRazorView : IView
    {
        private readonly RazorView view;

        public CustomRazorView(RazorView view)
        {
            this.view = view;
        }

        public void Render(ViewContext viewContext, TextWriter writer)
        {
            var modelSelector = viewContext.ViewData.Model as ModelSelector;
            if (modelSelector == null)
            {
                // This is not a widget, so fall back to stock-standard MVC/Razor rendering
                view.Render(viewContext, writer);
                return;
            }

            // We need to work out what @model is on the view, so that we can pass the correct model to it. 
            // We can do this by using reflection over the compiled views, since Razor views implement a 
            // ViewPage<T>, where T is the @model value. 
            var compiledViewType = BuildManager.GetCompiledType(view.ViewPath);
            var baseType = compiledViewType.BaseType;
            if (baseType == null || !baseType.IsGenericType)
            {
                throw new Exception(string.Format("When the view '{0}' was compiled, the resulting type was '{1}', with base type '{2}'. I expected a base type with a single generic argument; I don't know how to handle this type.", view.ViewPath, compiledViewType, baseType));
            }

            // This will be the value of @model
            var modelType = baseType.GetGenericArguments()[0];
            if (modelType == typeof(object))
            {
                // When no @model is set, the result is a ViewPage<object>
                throw new Exception(string.Format("The view '{0}' needs to include the @model directive to specify the model type. Did you forget to include an @model line?", view.ViewPath));                    
            }

            var model = modelSelector.GetModel(modelType);

            // Switch the current model from the ModelSelector to the value of @model
            viewContext.ViewData.Model = model;

            view.Render(viewContext, writer);
        }
    }
}

Механизм просмотра зарегистрирован, помещая его в Global.asax.cs:

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new ModelSelectorEnabledRazorViewEngine());

Rendering

Мой домашний просмотр включает в себя следующие строки, чтобы проверить все:

@Html.Action("Show", "Widget", new { widgetName = "Clock" })
@Html.Action("Show", "Widget", new { widgetName = "News" })

Ответ 2

Одним из вариантов было бы расширить идею частичных запросов в вашем приложении. Стив Сандерсон фантастический пример этого, хотя сообщение относится к MVC 1 и 2. Я думаю, что это все равно поможет вам v3, но я не исследовали v3, чтобы увидеть, выполнила ли команда MVC свою собственную версию. В вашем асинхронном сценарии вам нужно немного поиграть с реализацией, возможно, изменить определение PartialRequest, чтобы при необходимости принимать различную информацию, но я думаю, что это может быть хорошим началом. Конечным результатом было бы более полное устранение проблем, позволяющих отдельным контроллерам управлять определенным типом частичных и, в свою очередь, лучше знать модель, с которой вы хотите работать.

Ответ 3

Я не уверен на 100%, что это то, что вы искали, но атрибут [ChildActionOnly] может быть добавлен к методу внутри вашего контроллера. Это требует, чтобы метод можно было вызывать только из partial view. Затем вы можете настроить частичное представление для этого метода, который в основном напоминает один из ваших виджетов. Ознакомьтесь с примером MVC Music Store здесь:

http://www.asp.net/mvc/tutorials/mvc-music-store-part-10

Ответ 5

Я писал о том, чтобы делать именно это. См. http://blogs.planetcloud.co.uk/mygreatdiscovery/?tag=/widget

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

По умолчанию все виджеты имеют набор свойств KVP (я считаю, что это то, что имеет OP). Поэтому для простого виджета мы получаем доступ к этим свойствам из представления. Я использовал для виджета, который отобразил некоторый html (где html хранился в одном из этих свойств).

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

Здесь выполняется действие контроллера. Проверьте сообщения для полной информации.

[HttpGet]
public ActionResult Get(string name)
{
    var widget = widgetService.GetWidgetBySystemName(name, true);

    if (widget == null)
        return Content(string.Format("Widget [{0}] not found!", name));

    if (!this.ViewExists(widget.WidgetName))
        return Content(string.Format("A template for widget [{0}] was not found.", widget.WidgetName));

    if (widget is IWidgetWithDisplayModel) {
        (widget as IWidgetWithDisplayModel).CreateDisplayModel();
    }

    return PartialView(widget.WidgetName, widget);
}