Рецепт идеального приложения

Blazor… Сколько шума наделала эта технология. Все хотели посмотреть на вторую попытку Microsoft привнести C# в создания интерфейсов. Не уже ли у них это получилось? Давай разберемся.

Как всегда, я создам приложения, но на этот раз не буду создавать приложения по шаблону Blazor App. На этот раз я создам приложения по типу ASP.NET Core MVC и добавлю в него Blazor Component.

Моё приложения будет эмитирует онлайн кинотеатр.

Первое что надо сделать - это создать новый проект по типу Empty.

Empty template

При создании проект обратите особое внимания на версию .Net Core. Версия должна быть >= 3.1.x

Далее нужно настроить приложения для работы как с инфраструктурой MVC и Blazor.

Для этого добавим в класс Startup.cs следующее:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        services.AddServerSideBlazor();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseStaticFiles();
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{Controller=Home}/{Action=Index}"
            );
            endpoints.MapBlazorHub();
        });
    }
 }

Первое, что бросается в глаза (ну лично для меня) так это то, что в версии .Net Core 3.1 изменилось подключения сервисов необходимое для работы MVC.

Теперь вместо

 services.AddMvc();

мы имеем

services.AddControllersWithViews();

Второе нововведение — это изменение настройки маршрутизации.

В .Net Core 2.x настройка маршрутизации проходила следующим образом:

app.UseMvc(routers =>
{
    routers.MapRoute(
        name: null,
        template: "{controller=Home}/{action=Index}/{id?}");
});

В новой версии .Net Core настройка маршрута происходит следующим способом:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{Controller=Home}/{Action=Index}"
    );
    endpoints.MapBlazorHub();
});

Как по мне такой вариант инициализации машрутов лучше отображается действительность. По мимо MVC приложения также имеются и другие типы приложения. Например: Razor Pages, Web API и Blazor. Именно, поэтому настройка маршрута через метод, который называется UseMvc(), не логична.

Далее необходимо подготовить основную модель для онлайн кинотеатра, которая будет отображаться реальный фильм:

public class Movie
{
    public int Id { get; set; }

    public string Name { get; set; }

    public string Description { get; set; }

    public MovieType Type { get; set; }
}

Так же надо ввести типы имеющихся фильмов. Это можно сделать с помощью MovieType:

public enum MovieType
{
    Action,
    Western,
    Comedy,
    Horror
}

Далее нужно реализовать отображения фильмов, которые имеются в кинотеатре. Для этого воспользуемся Blazor Component. Компонент будет принимать модель фильма, отображать имя и описания конкретного фильма ну и с эмитируем постер фильма (для этого я буду отображать “заглушку”). Так же компонент должен реагировать на взаимодействия с мышью, т.е. изменить при наведения мыши.

В итоге получилось так:

@inject NavigationManager NavigationManager

<div class="card w-25 p-1 m-1 @Class" @onmouseover="OnMouseOver" @onmouseout="OnMouseOut" @onclick="@(()=>OnClick(Model.Id))">
    <svg class="bd-placeholder-img card-img-top" width="100%" height="180"
         xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice"
         focusable="false" role="img" aria-label="Placeholder: Image cap">
        <title>Placeholder</title>
        <rect width="100%" height="100%" fill="#868e96"></rect>
        <text x="40%" y="50%" fill="#dee2e6" dy=".3em">Image</text>
    </svg>
    <div class="card-body movie-body">
        <h5 class="card-title">@Model.Name</h5>
        <p class="card-text">@Model.Description</p>
    </div>
</div>

@code {
    [Parameter]
    public Movie Model { get; set; }

    public string Class { get; set; }

    public void OnMouseOver() => Class = "movie-selected";

    public void OnMouseOut() => Class = "";

    public void OnClick(int id) => NavigationManager.NavigateTo($"/movie/card?movieId={id}", true);
}

Из исходного кода компонента видно, что он не представляет ничего сложного. В данном компоненте происходит обработка таких событий как: onmouseover, onmouseout и onclick.

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

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

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

<base href="~/" />

Результат:

Movie card

После этого я реализовал фильтрацию фильмов по категориям (по типам) и открытие “карточки” фильма. Для этого я создал MovieController.cs, которые имеет следующее методы:

public class MovieController : Controller
{
    private readonly ICinemaRepository _repository;
    public MovieController(ICinemaRepository repository)
        => _repository = repository;

    public IActionResult List(MovieType type)
    {
        ViewBag.Title = type;
        return View(_repository.GetMovies(type));
    }

    public IActionResult Card(int movieId)
    {
        var movie = _repository.GetMovie(movieId);
        ViewBag.Title = movie.Name;
        return View(movie);
    }
}

Код реализации репозитория можно посмотреть в GitHub.

У каждого фильма есть свои оценки/комментарии от пользователей. Я хочу реализовать что-то подобное и в моем приложение. Для начало создадим сущность, которая будет отображать комментарии для определенного фильма. Данная сущность показана ниже

public class Comment
{
    public int Id { get; set; }

    public int MovieId { get; set; }

    public string Author { get; set; }

    public string Value { get; set; }

    public DateTime CreatedOn { get; set; }
}

Так же я реализую контроллер, который будет возвращать комментарии для определённого фильма и добавлять их. Ниже показать код контроллера CommentController.cs:

public class CommentController : Controller
{
    private readonly ICinemaRepository _repository;
    public CommentController(ICinemaRepository repository)
        => _repository = repository;

    public IEnumerable<Comment> GetComments(int movieId)
        => _repository.GetComments(movieId);

    [HttpPost]
    public IActionResult AddComment([FromBody]Comment comment)
    {
        _repository.AddComment(comment);
        return Ok();
    }
}

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

@using System.Net.Http
@inject IHttpClientFactory HttpClientFactory

<EditForm Model="@NewComment" OnSubmit="OnCreateNewComment">

    <div class="form-group">
        <label>Author</label>
        <InputText class="form-control" @bind-Value="NewComment.Author" />
    </div>

    <div class="form-group">
        <label>Comment</label>
        <InputTextArea class="form-control" @bind-Value="@NewComment.Value" />
    </div>

    <div class="float-right">
        <button class="btn btn-success" type="submit">Add</button>
    </div>

</EditForm>

@if (Comments == null)
{
    <span>Loading...</span>
}
else
{
    @foreach (var comment in Comments)
    {
        <div>
            <div>
                <span class="comment-author">@comment.Author</span>
                <span class="comment-createdate">@comment.CreatedOn.ToShortDateString()</span>
            </div>
            <div>@comment.Value</div>
        </div>

        <br />
    }
}

@code {
    private HttpClient _client;

    public List<Comment> Comments { get; set; }

    [Parameter]
    public int MovieId { get; set; }

    public Comment NewComment { get; set; } = new Comment();

    public async Task OnCreateNewComment()
    {
        NewComment.CreatedOn = DateTime.Now;
        NewComment.MovieId = MovieId;
        Comments.Add(NewComment);

        var result = await _client.PostAsync("/Comment/AddComment", 
            new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(NewComment), System.Text.Encoding.UTF8, "application/json"));

        NewComment = new Comment();
    }

    protected override async Task OnInitializedAsync()
    {
        _client = HttpClientFactory.CreateClient("Cinema Api");

        var response = await _client.GetAsync($"/Comment/GetComments?movieId={MovieId}");
        var content = await response.Content.ReadAsStringAsync();

        Comments = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Comment>>(content);
    }
}

Как видно из кода выше CommentsComponent.razor представляет из себя EditForm и список уже существующих комментариев. EditForm - это стандартный компонент, который представляет из себя надстройку над существующей form из Html.Это надстройка позволяет реагировать на submit только, когда Model прошла валидацию (OnValidSubmit=””) или наоборот, когда модель не прошла валидцию (OnInvalidSubmit=””). Эти методы дают возможность контролировать внешний вид компонента при валидации модели. Но в я использую услвоную нажатие на кнопку без проверки входящей модели.

Так же в секции @code видно два метода, которые взаимодействуют с сервером при помощи HttClient. Первое взаимодействия происходит в методе OnInitializedAsync. В данном методе происходит запрос всех комментариев по данному фильму. Второе взаимодействия происходит в методе OnCreateNewComment. В этом методе я отправляю новый комментарий на сервер.

Ниже можно посмотреть результат того:

Comments

Вместо вывода:

Blazor+ASP.NET Core MVC сдружить можно, но у меня возникли некоторые проблемы с этим. Первое с чем я столкнулся это то, что в к версии .NET Core < 3.1.x есть проблема при передачи параметра в компонент, т.е. при таком виде

@(await Html.RenderComponentAsync<MovieCard>(RenderMode.ServerPrerendered, new { Model = movie }))

происходило исключения. Это проблема решается путем обновления .NET Core до версии 3.1.

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

В целом мне очень понравилось комбинировать ASP.NET Core MVC и Blazor Component. Надеюсь, при написание вашего проекта у вас не возникнет таких проблем.

Ссылка на репозиторий.

Написано 6 февраля 2020