Как задать модель для шаблона?

252
28 февраля 2018, 06:12

Есть сайт на ASP.Net Core MVC. У него в шапке всегда есть выдвижной пункт меню со списком жанров, также там есть блок авторизованного пользователя (аватарка, ник, кнопка разлогина).

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

public class BaseViewModel
{
    private ISession session;
    private TMDbContext context;
    public BaseViewModel(ISession session, TMDbContext context)
    {
        this.session = session;
        this.context = context;
    }
    public User CurrentUser => session?.GetJson<User>("CurrentUser");
    public IQueryable<Genre> Genres => context.Genres;
}

Но при попытке создать первый же производный класс и использовать его в контроллере сталкиваешься с некоторыми проблемами. Если сессию легко получить в каждом контроллере без каких-либо манипуляций из HttpContext.Session, то контекст у тебя в контроллере вообще не фигурирует явно. Все данные у меня идут через реализации интерфейсов (по рекомендации из учебника Фримена), с привязкой в Startup.cs типа
services.AddDbContext<TMDbContext>(option => option.UseNpgsql( Configuration["Data:Movies:ConnectionString"]));

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

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

Как и где делать привязку данных, которые нужны для шаблона, т.е. при отображении любой View?

Класс связки данных с контекстом БД:

public interface IUserRepository
{
    IQueryable<User> Users { get; }
    IQueryable<UserRate> UserRates { get; }
    void SaveUser(User user);
    void SaveUserRate(UserRate userRate);
}
public class EFUserRepository : IUserRepository
{
    private TMDbContext context;
    public EFUserRepository(TMDbContext context)
    {
        this.context = context;
    }
    public IQueryable<User> Users => context.Users;
    public IQueryable<UserRate> UserRates => context.UserRates;
    public void SaveUser(User user)
    {
        context.Users.Add(user);
        context.SaveChanges();
    }
    public void SaveUserRate(UserRate userRate)
    {
        context.UserRates.Add(userRate);
        context.SaveChanges();
    }
}

Вызов этого класса в конструкторе контроллера:

public class UserController : Controller
{
    private IUserRepository repository;
    public UserController(IUserRepository repository)
    {
        this.repository = repository; // в Startip.cs привязка через: services.AddTransient<IMovieRepository, EFMovieRepository>();
    }
    public IActionResult Index()
    {
        return View();
    }
}
Answer 1

На самом деле, нет ничего плохого в том чтобы запросить жанры из вида минуя контроллер. Ведь контроллер - это компонент который обрабатывает пользовательский ввод (в случае веб-приложений - это HTTP-запросы), он не является почтальоном между моделями и видами.

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

В соответствии с документацией, для этого можно использовать директиву @inject:

@inject IGenresRepository Genres
<ul>
    @foreach (var genre in Genres.All) {
        <li>@genre.Name</li>
    }
</ul>
Answer 2

Основа MVC - это разделение отображения данных и логики получения данных.

Смотрите. У вас есть какой-то контроллер, например, FilmsController, в нём есть Action отображающий страницу детального описания фильма, скажем, Detail.

Правильный порядок действий в этом Action: считать всё из базы данных, наполнить модель данными, отключиться от базы, передать модель во View.

За попытки залезть в базу данных из View или Model нужно расстреливать на кодревью и заставлять тысячу раз писать мелом на доске "Смысл MV*-паттернов в разделении кода бизнес-логики и отображения". Не надо во вью или модели рассчитывать, что у вас есть доступ к базе на "щас ещё чуть-чуть дочитаю", раньше надо было.

Поэтому ваша базовая модель, если вам реально ВЕЗДЕ нужен список жанров должна выглядеть так:

public class BaseViewModel
{
    public IReadOnlyCollection<Genre> Genres { get; set; }
}

Всё, наследуйтесь на здоровье.

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

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

Но понятное дело, что гораздо проще обойтись вообще без сервисов и репозиториев, создавать в базовом контроллере контекст и пользоваться этим контекстом в каждом контроллере напрямую. Кодогенерация в Asp.Net MVC как раз содержит пример такого CRUD-контроллера.

Update. По поводу замечания Павла Майорова:

Почему контроллер вообще должен знать что на странице есть шапка со списком жанров? Шапка - это ответственность исключительно представления, а не контроллера. Должна быть возможность сменить шапку у сайта не переписывая все контроллеры!

В общем, я считаю что выборка всех данных в контроллере нарушает SRP.

В принципе, можно сделать ещё более правильным образом.

Во-первых, создайте контроллер GenreController, пропишите нужный метод получения жанров и пометьте его атрибутом [ChildActionOnly]

Где-то в общем Layout страницы в div'е с шапкой страницы можете вставить показ списка жанров через @Html.Action(метод, контроллер жанров).

READ ALSO
Защищённая децентрализованная сеть

Защищённая децентрализованная сеть

Доброго времени сутокПытаюсь писать диплом на тему "Разработка приложения многофакторной аутентификации для защищённой децентрализованной...

213
Как задать значение смещения для строки при её форматировании

Как задать значение смещения для строки при её форматировании

Нужно на выходе получить некую таблицу

295
Запись и подсчёт повторяющихся строк C#

Запись и подсчёт повторяющихся строк C#

ЗдравствуйтеЯ получаю из запроса список объектов

236
Онлайн консультант на Asp.net, js и php

Онлайн консультант на Asp.net, js и php

Добрый день! Есть некий сайт написанный с помощью YiiНа данный момент к нему подключен с помощью скрипта на JS онлайн консультант, которых в интернете...

224