Способ первый. Использование вложенных представлений
Оптимальное решение, на мой взгляд - использование контролов, или встроенных представлений. Однако и оно не без греха. Но рассмотрим все по порядку.
В MS MVC существует класс под названием ComponentController. Это, по сути, обычный контроллер, использующий немного другие методы и сигнатуры. Зачем понадобилось делать дополнительный класс с почти такой же функциональностью, для меня осталась загадка. Итак, вот как используется этот класс:
public class CategoryController : ComponentController
{
public ActionResult Index()
{
// Add action logic here
throw new NotImplementedException();
}
public void RenderControl(string CategoryName)
{
RenderView(CategoryName);
}
}
Т.е. видим два существенных отличия: класс наследует ComponentController, а не просто Controller, и второе - по-другому выглядят методы действия. А выглядят они точно также, как выглядели методы действия обычных контроллеров раньше - т.е. возвращают void и рендерят представление с помощью RenderView.
В данном случае имя представления передается как параметр, что позволяет вызывает вызывать представления абсолютно произвольно, используя при этом всего один метод контроллера.
Размещается контроллер вместе с остальными в папке Controllers (думаю, на самом деле эти файлы можно размещать где угодно). А вот с представлением не так все просто. Оно должно лежать в специальной папке Components/[ComponentControllerName]/Views/[ViewName]. Т.е. в корне вы создаете папку Components, в ней произвольную папку, в которой создаете папку Views, а уже сюда кладете файлы представлений.
Дальше все просто. Контроллер компонента создан, представление - тоже, остается только его вызвать. У меня это сделано так: главная страница загружает выпадающий список, в нем на onchange вызывается функция JavaScript, которая с помощью библиотеки Prototype загружает представление с соответствующими параметрами (подробности работы с Prototype смотри в другой записи блога тут). А вот это представление содержит вызов компонента.
Вызов можно осуществлять двумя способами. Во-первых, используя RenderUserControl() - тогда сам вызов будет выглядеть так:
<%= Html.RenderUserControl("~/Views/SomeView.aspx") %>
Как видите, здесь указывается непосредственно имя файла. В ряде случае это может быть совершенно неудобно. Например, когда мы на стадии этого вызова еще не знаем имя файла, или хотим передать другой параметр, а метод чтобы сам решал, какое представление ему вызывать. Для этого используем второй способ:
<% =Html.RenderComponent<CategoryController>(c => c.RenderControl("Public")) %>
Здесь мы просто указываем имя представления, а могли бы, например, передавать какой-нибудь идентификатор. Мы можем брать из модели, загруженной в объект ViewData, любой доступный член. Почему бы не передавать его в качестве параметра, например, вот так:
<% =Html.RenderComponent<CategoryController>(c => c.RenderControl(View.Model.PageId)) %>
К сожалению, подобный код не будет работать (из-за бага в MVC). Вы просто увидите ошибку следующего содержания:
Unable to cast object of type 'System.Linq.Expressions.MemberExpression' to type 'System.Linq.Expressions.ConstantExpression'.
Выход нашел некто Chris Sainty (ссылка на его статью в конце заметки). Просто добавляем следующий метод в файл вызывающего представления, и вместо стандартного Html.RenderComnonent вызываем его:
protected string RenderComponent(Expression<Action<CategoryController>> action)
{
var controller = new CategoryController();
controller.Context = this.ViewContext;
var ex = action.Compile();
ex.Invoke(controller);
return controller.RenderedHtml;
}
Вызов будет выглядеть соответственно:
<% =RenderComponent(c => c.RenderControl(ViewData.Model.ParentCategoryName)) %>
Все работает!
Способ второй. Подтягиваем разметку Html из базы данных
Первый способ, безусловно, хорош. Но у него есть один существенный недостаток - что, если состав вложенных контролов постоянно меняется, т.е. попросту говоря, не является детерменированным? Ничего такого, надо просто каждый раз обновлять версию dll на сервере, и все. Тоже выход, но можно ли обойтись без этого? Можно - например, если хранить код разметки в поле таблицы. Этот вариант подходит для не очень большой и сложной разметки, но зато позволяет обойтись без постоянного деплоя.
Как это сделать? Очень просто - не забываем, что мы можем передать представлению любой класс, а уже в этом классе не составит труда загрузить любое поле из таблицы. Например, так:
private string html;
public CategoryParameters(int? ParentCategoryId)
{
using (DataCoreConnection context = new DataCoreConnection())
{
var htm = (from p in context.ItemType
where p.Id == ParentCategoryId
select p.ParametersHTML).ToList();
if (((List<string>)htm).Count > 0)
html = ((List<string>)htm)[0];
}
}
public string Html
{ get { return html;} }
А вот так выглядит вызов:
<%=ViewData.Model.Html %>
Третий способ. Подтягивание представления с помощью Ajax
Подробно о том, как использовать Ajax в проектах MVC, рассказано в другой моей заметке (тут), здесь я лишь покажу, как это использовать в наших целях. Вот код JavaScript, который вызывает представление:
function LoadCategory(parentCategory)
{
var newDiv = document.createElement("div");
newDiv.id = "ControlDiv_" + parentCategory;
newDiv.className = "ControlBlock";
var url = "Controls/GetCategories/" + parentCategory;
new Ajax.Updater(newDiv, url,
{
method: 'get'
}
);
$('Container').appendChild(newDiv);
}
Т.е. здесь мы динамически создаем блок DIV, в который загружаем содержимое представления, а сам блок помещаем в ранее созданный DIV с Id= Container.
На самой странице же достаточно сделать такой вызов:
<script language="javascript" type="text/javascript">
LoadCategory(1);
</script>
Полезные ссылки:
Статья Chris Sainty, посвященная RenderComponent() и RenderUserControl()
Using the ComponentController in ASP.NET MVC CTP 2 - Mike Bosch