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

При изучении ASP.NET MVC у меня всегда возникал вопрос:

А почему в качестве "веб-морды" нельзя использовать клиента, написанного с помощью синтаксиса Razor?

И на протяжении нескольких лет это было нельзя сделать.

Собственно, а теперь это можно сделать с помощью Blazor Server.

Создадим 3 звенное приложение: клиент- сервер - база данных (на самом деле будет 2 звенное приложение без БД).

В качестве клиента будет выступать Blazor Server, в качестве “бэка” будет выступать ASP.NET Core 3.1.

Для иллюстрации возможностей Blazor Server в качестве веб-клиента, я создам приложения для работы с заметками.

Для начало создам сервер. Для создания сервера буду использовать шаблон “Empty”.

Empty template

Далее необходимо создать инфраструктуру для работы приложения как Web Api. Для этого я создам директорию /Models. Понятно из названия, что в данной папке будет лежат данные, которые необходимы для приложения.

Web Api

Models

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

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

        public string Title { get; set; }

        public string Description { get; set; }
    }

Сущность заметки не представляет из себя ничего сложного: id - идентификатор объекта в БД title - наименования объекта description - “тело” заметке

Repository

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

    public interface INoteRepository
    {
        //Возвращение всех заметок

        Task<List<Note>> GetAllNotes(CancellationToken token);

        //Возвращение конкретной заметки (по индентификатору)

        Task<Note> GetNote(int noteId, CancellationToken token);

        //Добавление или обновление заметки

        Task AddOrUpdateNote(Note note, CancellationToken token);

        //хз что😁

        Task DeleteNote(int noteId, CancellationToken token);
    } 

Реализацию самого репозитория можно посмотреть по ссылки.

Controller

Без лишних слов, код контроллера

    [ApiController, Route("api/notes")]
    public class NotesController : ControllerBase
    {
        private readonly INoteRepository _repository;
        public NotesController(INoteRepository repository)
            => _repository = repository;

        [HttpGet, Route("list")]
        public Task<List<Models.Note>> GetAllNotes(CancellationToken token)
            => _repository.GetAllNotes(token);

        [HttpGet, Route("{noteId}")]
        public Task<Models.Note> GetNote(int noteId, CancellationToken token)
            => _repository.GetNote(noteId, token);

        [HttpPost, Route("createOrUpdate")]
        public async Task<IActionResult> GetNote(Models.Note note, CancellationToken token)
        {
            await _repository.AddOrUpdateNote(note, token);
            return Ok();
        }
        
        [HttpDelete, Route("delete/{noteId}")]
        public async Task<IActionResult> DeleteNote(int noteId, CancellationToken token)
        {
            await _repository.DeleteNote(noteId, token);
            return Ok();
        }
    }

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

Startup

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

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<INoteRepository, NoteRepsitory>();
            services.AddControllers();
        }

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

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }

Время проверки моей “Апишки” через Postman.

Web Api

Отлично! Контроллер отдает данные.

Но в текущей реализации есть один недостаток. Если я вызову мето api/notes/note/{number}, где number - идентификатор, которого нет, то я получу не красивую ошибку.

Error

Такая подробная информация очень полезная при отладке и её нужно записать в лог, но она абсолютно не нужна для клиента.

Для того, что выводить клиенту нужное сообщение я буду использовать Middleware. Суть промежуточного ПО в этом случаи, что я буду вызывать метод из MVC и ловить исключении, которые возникнуть входе исполнение Action’a.

Microsoft любезно предоставила возможность создавать Middleware как угодно. Единственное ограничение: пользовательский Middleware должен принимать RequestDelegate и иметь метод с сигнатурой:

 Task Invoke(HttpContext context)

Вот само промежуточное ПО:

    public class HttpExceptionMiddleware
    {
        private readonly RequestDelegate _next;
        public HttpExceptionMiddleware(RequestDelegate dlt)
            => _next = dlt;

        public async Task Invoke(HttpContext context)
        {
            try
            {
                await _next(context);
            }
            catch (Exception ex)
            {
                await context.Response.WriteAsync(ex.Message);
            }
        }
    }

И добавляем наш Middleware в класс Startup.cs:

 app.UseMiddleware<HttpExceptionMiddleware>();

И как результат я имею красивое сообщения для клиента об ошибке.

The nice message for client

Client

Как я писал выше в качестве клиента я буду использовать Blazor Server.

Для начала создадим проект на .NET Core 3.1 по типу Empty.

Первое что надо сделать настроить его для работы с Blazor. Для этого я внесу изменение в класс Startup.

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

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

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

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapFallbackToPage("/_Host");
                endpoints.MapBlazorHub();
            });
        }
    }

После этого нужно создать директорию Pages. В данной директории будет храниться хост для запуска компонентов Blazor.

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

После внесенных изменений решение выглядит следующим образом:

The solution

Я подготовил проект для старта и запуска Blazor. Далее нужно реализовать сам интерфейс.

Я хочу сделать приложение похожее на изображение ниже:

The design

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

Судя по всему мне нужно реализовать 3 компонента: лист с заметками, компонент самой заметки и карточка заметки.

Компонент заметки — это то, как заметка будет представлена в списки, а карточка заметки — это отображение заметки, когда пользователь "кликает" на неё

Первым делом я создам компонент NoteComponent.razor, который будет отвечать за отображение заметки в списки. Данный компонент будет принимать в качестве параметра объект Note, будет менять стиль при взаимодействии с указателем мыши, и будет выкидывать событие о клике.

Без лишних слов, код:

<div class="@Class border-bottom" @onmouseover="OnMouseOver" @onmouseout="OnMouseOut" @onclick="@(()=>OnMouseClick(Model.Id))"
     pag-action="Edit" page-model="@Model.Id">
    <div class="note-title">
        @Model.Title
    </div>
    <div class="note-body">
        @Model.Description
    </div>
</div>

@code {

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

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

    public Task OnMouseClick(int noteId) => OnClickNoteCallBack.InvokeAsync(Model);

    [Parameter]
    public Note Model { get; set; }

    [Parameter]
    public EventCallback<Note> OnClickNoteCallBack { get; set; }

    public string Class { get; set; }
}

Далее необходимо реализовать компонент, который отображаем список всё заметок, но для начало нужно реализовать сервис для “общение” с WebApi.

Сервис должен получать данные от WebApi по http, для этого ему нужен HttpClient. Т.к. при Dispose объекта HttpClient не всегда освобождается порт, я воспользуется новой возможностью, который появился в .Net Core, IHttpClientFactory.

У HttpClient нет методов, которые позволили мне отправлять и получать данные от сервера в формате Json. Для того, чтобы получить данные в нужном мне формате мне пришлось бы сначала сделать запрос к сервера, затем считать контент и только потом десериализовать его. А т.к. сервис, в клиенте, будет неоднократно получать и отправлять данные серверу, то придется писать данный код много раз. Чтобы этого не дать я вынес данный код в методы расширения для HttCliet.

    public static class HttpClientExtensions
    {
        public static async Task<T> GetJsonAsync<T>(this HttpClient httpClient, string requestedUrl)
        {
            var response = await httpClient.GetAsync(requestedUrl);

            if (!response.IsSuccessStatusCode)
                throw new HttpRequestException($"Can't call {requestedUrl}; Status code {response.StatusCode}");

            var content = await response.Content.ReadAsStringAsync();
            return JsonConvert.DeserializeObject<T>(content);
        }

        public static async Task<bool> PostJsonAsync<T>(this HttpClient httpClient, string requestedUrl, T obj)
        {
            var stringContent = new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json");
            var response = await httpClient.PostAsync(requestedUrl, stringContent);
            return response.IsSuccessStatusCode;
        }
    }

Сам сервис, который находится в клиенте:

    public class NoteService : INoteService, IDisposable
    {
        //Адрес сервера

        private const string Server = "https://localhost:44350/api/";
        private readonly HttpClient _httpClient;

        public NoteService(IHttpClientFactory httpFactory)
            => _httpClient = httpFactory.CreateClient();


        public async Task CreateOrUpdate(Note note)
        {
            const string method = "notes/createOrUpdate";
            await _httpClient.PostJsonAsync<Note>($"{Server}{method}", note);
        }

        public Task<List<Note>> GetNotes()
        {
            const string method = "notes/list";
            return _httpClient.GetJsonAsync<List<Note>>($"{Server}{method}");
        }

        public async Task Delete(int noteId)
        {
            const string method = "notes/delete/";
            await _httpClient.DeleteAsync($"{Server}{method}{noteId}");
        }

        public void Dispose()
            => _httpClient?.Dispose();
    }

Как по мне выглядит очень симпатично.

В самом компоненте нет ничего сложного. Если интеренсо то реализацию можно посмотреть тут.

Вот и результат:

The list of notes

Все что осталось для того, что реализовать то, что я хотел изначально, так это карточку заметки.

В карточке нужно отображать свойства объекта такие как: Title, Description. Так же в карточке должна быть возможность управлять конкретной заметки, а именно обновлять или удалять её.

Всё это реализовано здесь.

Результат карточки:

The list of notes

Заключение:

Наконец-то появилась возможность реализовывать Web-клиента на Razor.

Из основных недостатков можно отметить, что общение между сервером (где сидит клиент) и самим представление приходит через html. Если реализовать интерфейс, в котором есть 2^10 строчек в таблице. И при каждом клике добавляется ещё столько же. В этом случаи сервер будет гонять мегабайты html.

Как всегда, ссылка на репозиторий.

Подробнее

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

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. Надеюсь, при написание вашего проекта у вас не возникнет таких проблем.

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

Подробнее

Blazor

Недавно Microsoft представила .NET Core 3, а вместе с ним и Blazor - новый famework для написания Web-приложений. Прежде всего Blazor рассчитан на Fullstack-программистов, которые работали с ASP.NET MVC или/и ASP.NET Pages, но в отличие от данных Framework’ов в Blazor вся логика изменения UI реализована на всеми любимом C#. Теперь для “оживления” Web-сайта можно минимизировать использование js.

Как удалось реализовать это Microsoft?

Microsoft давно уже хотела “запихнуть” C# в Frontend. Ещё в далеком 2002 года Microsoft представила миру первую реализацию ASP.NET Web Forms. ASP.NET Web Forms была хороша, как и Win Forms, для своего времени. Но ASP.NET Web Forms не прошел проверку временем. Microsoft решила абстрагировать разработчиков Web Forms от самих Web-технологий. Таких как: HTTP, HTML, CS и т.д. В надежде, что миллионы разработчиков дескторных приложений для Windows перейдут на Web-приложения. Но с развитием Web’a ASP.NET Web Forms умер. И основные причины этому:

  • Тяжеловесное состояние страница - современные Web-приложения требовали динамического контента, и в ASP.NET Web Forms решили эту проблему путем передачи гигантских данных между клиентом и сервером при любом изменение страницы;
  • Отсутсвие четкого шаблона проектирования - за счет этого тестировать и отлаживать enterprise-приложения становилось тяжело, а иногда и просто не реально;
  • Ограниченный контроль на HTML версткой - создание пользовательских элементов управления было не из легких задач.

Понимая это Microsoft переосмыслила framework для написания Web-приложений. Первым делом убрали абстракцию, теперь разработчик четко понимал что он разрабатывает Web-приложения, а не десктопное приложения в браузере. Как только убрали абстракцию то появился полный контроль над HTML,CSS и js. И это всё было подано с MVC паттерном. Так появился ASP.NET MVC.

Отличная Web-технология, которая позволяет писать хорошие приложения. ASP.NET MVC включает в себя Razor - движок представлений. Язык разметки Razor позволяет генерировать HTML страницы со вставками C# кода. Но для того, чтобы создать динамически изменяемый контент нужно использовать js.

И совсем недавно Microsoft представила новый framework Blazor. Blazor основан на технологии WebAssembly , которая позволяет запускать код, написанный на любом языке, в браузере. Это происходит за счет интерпретации кода в байт-кода.

Microsoft разрабытвает несколько видов приложений на Blazor:

  • Blazor Client - это платформа для SPA-приложений, которая позволяет создавать интерактивные клиентские веб-приложения с помощью .NET. Это происходит благодаря загрузке .NET Framework’a и .NET Runtime на клиент (в настоящее время доступна только предварительная версия);
  • Blazor Server - предоставляет поддержку размещения компонентов Razor на сервере в приложении ASP.NET Core, а обновления пользовательского интерфейса передаются через подключение SignalR

Для того, чтобы пощупать Blazor я создам приложение для ведение дел ToDos. Для начало выберу новый шаблон проекта, который доступен в Visual Studio:

New Blazor template

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

А вот первое, что бросается в глаза это способ изменение настройки маршрутизации для приложения на Blazor в сравнение с настройка маршрутизации для приложения ASP.NET MVC. Для наглядности ниже я размещу код Configure :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
     if (env.IsDevelopment())
     {
         app.UseDeveloperExceptionPage();
     }
     else
     {
         app.UseExceptionHandler("/Error");
     }

     app.UseStaticFiles();

     app.UseRouting();

     app.UseEndpoints(endpoints =>
     {
         endpoints.MapBlazorHub();
         endpoints.MapFallbackToPage("/_Host");
     });
}

Как видно из кода выше в Configure отсутствует настройка route, а вместо неё есть настройка endpoints. Данный подход взять из ASP.NET Pages. А за routing отвечает специальный компонент Router, который находится в App.razor:

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

Blazor как и Angular основан на компонентах, что упрощает повторное использование одно функционала в разных местах. Для меня, как для человека, который пришел в мир Web’a, из мира разработки десктопных и мобильных приложений на Windows, компонент - это UserControl из WPF или UWP. И с этой ассоциацией мне было легче понять, что такое компонент.

Основной компонент Route располагает (хостится) на обычной razor-странице. Ниже показан код _Host.cshtml:

@namespace ToDos.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ToDos</title>
    <base href="~/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/site.css" rel="stylesheet" />
</head>
<body>
    <app>
        @(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
    </app>

    <script src="_framework/blazor.server.js"></script>
</body>
</html>

Единственное, что меня смутило так это, что рендер компонента происходит через HTML-хэлпер. Скорей, всего в дальнейшем Microsoft откажется от такого подхода в сторону tag-хэлперов, и вместо строчки :

@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))

Будет строчка:

<Component name="App" mode="ServerPrerendered"/>

UPD: На сайту Microsoft можно увидеть текущею реализацию (https://docs.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-3.1)

<component type="typeof(Counter)" render-mode="ServerPrerendered" param-IncrementAmount="10" />

JS скрипт _framework/blazor.server.js используется для создания связи между сервером и клиентом через SignalR.

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

public class ToDoItem
{
    public string Title { get; set; }

    public bool IsDone { get; set; }
}

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

public class ToDoList
{
    public List<ToDoItem> Items { get; set; } = new List<ToDoItem>();

    public void AddNewToDo(ToDoItem todo)
    {
        Items.Add(todo);
    }

    public Task<List<ToDoItem>> GetToDoItems()
    => Task.Run(() =>
    {
       if (!Items.Any())
       {
          for (int i = 0; i < 10; i++)
          {
              Items.Add(new ToDoItem
              {
                  Title = $"Task #{i}",
                  IsDone = false
              });
           }
        }

        return Items;
     });
 }

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

public interface IToDoList
{
     void AddNewToDo(ToDoItem todo);

     Task<List<ToDoItem>> GetToDoItems();
}

Унаследую данный интерфейс классом ToDoList. И для того, чтобы система понимала экземпляр какого класса использовать при использование интерфейса IToDoList буду использовать встроенный контейнер внедрения зависимостей:

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddSingleton<WeatherForecastService>();
    services.AddTransient<IToDoList, ToDoList>();
}

Теперь создадим новый компонент для нашего списка дел.

New Blazor template

Любой Blazor-компонент состоит из двух блоков:

  • HTML-разметка - отвечает за внешний вид компонента;
  • Блок @code{} - отвечает за логику компонента.

Для начало я отображу текущий список дел. Для этого изменю ToDo.razor как показано ниже:

@page "/todo"
@using Microsoft.AspNetCore.Components
@using ToDos.Data
@inject IToDoList ToDoList

@if (toDoItems == null)
{
    <h6>>ToDo list is empty!</h6>
}
else
{
    <table>
        @foreach (var toDoItem in toDoItems)
        {
            <tr class="form-control">
                <td>
                    <input type="checkbox" checked="@toDoItem.IsDone" />
                </td>
                <td>@toDoItem.Title</td>
            </tr>
        }
    </table>
}

@code{
    List<ToDoItem> toDoItems;
    protected override async Task OnInitializedAsync()
    {
        toDoItems = await ToDoList.GetToDoItems();
    }
}

При инициализации данного компонента , метод OnInitializedAsync, происходит загрузка задач с помощью нашего сервиса ToDoList, при этом сервис я указал как абстракцию,а реализацию находит сама платформа:

@inject IToDoList ToDoList

New Blazor template

Но отображать списка дел можно и с помощью обычных razor-страниц, для этого Blazor не нужен. Для того, чтобы воспользоваться всеми прелестями нового framework’a реализую возможность добавления новых задач. Для этого в компонент помещу input и при нажатие на Enter будет создаваться новая задача.

<input class="form-control" text="@title" placeholder="Add a new todo" />

Создам новое приватное поле в блоке @code title, и укажу его в качестве text для только что созданного элемента input.

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

Изначально мой вариант выглядел так:

<input type="text" onkeypress=@KeyWasPressed />

@code {
  private void KeyWasPressed()
  {
    //TODO
  }
}

Это казалось мне логичными потому, что любые вставки C# кода в HTML происходят с помощью директивы @. Но это не сработало.

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

<input type="text" @onkeypress="KeyWasPressed" />

@code {
  private void KeyWasPressed()
  {
    //TODO
  }
}

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

<input class="form-control" text="@title" @onkeypress="OnKeyDown" placeholder="Add a new todo" />

@code{
string title;
public void OnKeyDown(KeyboardEventArgs aegArgs)
{
     if (aegArgs.Key == "Enter")
     {
        ToDoList.AddNewToDo(new ToDoItem { Title = title });
        title = string.Empty;
     }
}
}

Здесь меня ждал сюрприз. Текст, который вводит пользователь, не сохранялся.

Дело в том, что атрибут text=”@title” задавал значение свойству text элементу input при рендере страницы, но не обеспечивал связь между свойством элемента и полем блока @code.

Нужен был некий механизм привязки…

И да, данный механизм есть в Balzor. После не большего изменение, элемент input выглядит следующим образом:

<input class="form-control" @bind="@title" @onkeypress="OnKeyDown" placeholder="Add a new todo" />

Однако, после тестирование я выяснил, что значение поле title изменяется после потери фокуса на элементе input. Меня такой вариант не устраивал и после не большего пояска я нашел решение. После небольших исправлений код элемента input выглядит следующим образом:

<input class="form-control" @bind-value="@title" @bind-value:event="oninput" @onkeypress="OnKeyDown" placeholder="Add a new todo" />

Результат:

New Blazor template

Выглядит не плохо, с учетом того, что вся логика добавление новых задач реализована на C#.

Теперь во мне взыграл интерес: а что ещё я смогу сделать?

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

  • Не отображать завершенные задачи;
  • Показывать общие число отображаемых задач;
  • Возможность выбора отображения: показывать все задачи или показывать не завершенные задачи.

На мой взгляд внимание заслуживает последний пункт, а реализацию остальных возможностей можно посмотреть на GitHub’e.

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

<select class="form-control" @onchange="OnChangeSelect">
    <option selected>Choose...</option>
    <option value="true">Not show done tasks</option>
    <option value="false">Show all tasks</option>
</select>

@code{
bool? showNotDone;
public void OnChangeSelect(ChangeEventArgs args)
{
    var isNull = Boolean.TryParse(args.Value.ToString(), out var value);
    if (!isNull)
    {
        showNotDone = null;
        return;
    }
    showNotDone = value;
}
}

Свойства showNotDone будет отвечать за генерацию необходимого фильтра. Сам фильтр будет реализован с помощью метода:

public Func<ToDoItem, bool> FilterExpression()
{
    if (showNotDone == null || showNotDone.Value)
    {
       return x => !x.IsDone;
    }

    return x => !x.IsDone || x.IsDone;
}

И вот что получилось в конечно итоге:

New Blazor template

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

Мне кажется, что Blazor - это отличный framework, для написания интерактивного пользовательского интерфейса. Он сочетает в себе всё лучшее, что есть в мире ASP.NET, но при этом избавляется от недостатков и проблем, которые могли бы возникнуть при использование js.

Подробнее

Виртуализация приложения

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

application-virtualization

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

и какой от этого толк?

Наиболее популярным способом разбития монолитной архитектуры на микросервисы была виртуализация отдельных частей (сервисов) в виртуальных машинах (Hyper-V, VirtualBox).

Решение было хорошим до того момента, когда сервисы не начинали разрастаться. И для нормальной работы требовалось наращивание серверных мощностей.

И тут на помощь приходит Docker.

Он, в отличие от виртуальной машины, работает быстро и не требует много ресурсов. Всё потому, что Docker не “создает” гостевую ОС, а использует систему хоста. Для понимания нужно рассмотреть изображения ниже.

application-virtualization

При этом у каждого контейнера изолированная файловая система.

Docker работает только на Unix-системах, но благодаря WSL есть возможность запускать Docker на Windows (так же Docker можно запускать с помощью виртуализации, но с появлением WSL2 в этом нет смысла).

С 2015 года Microsoft меняет свою позицию по отношению к Open Source и другим сторонним технологиям. С появлением .Net Core есть возможность упаковывать веб приложения в контейнер Docker’a.

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

application-virtualization

После чего в тулбаре появляется возможность упаковать приложения в контейнер Docker’a.

application-virtualization

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

Как пример Azure Service Fabric.

Подробнее

Тестирование приложения ASP.NET Core

Для того, чтобы можно было протестировать ASP.NET Core приложения (и не только), нужно разбить приложения на логические части. Для этого используются архитектурные паттерны: MVC, MVVM, MVP и т.д. Для приложения, написаного на ASP.NET Core, используется паттерн MVC. Данный паттерн предполагает разделения структуры приложения на Model (Бизнес логика)-Controller (Посредник между БЛ и представлением)-View (Представление). Но чтобы приложение было легко маштабируемое, легко поддерживаемое и легко тестируемое нужно, чтобы уровни паттерна зависили от абстракции.

Но вернемся к теме моего поста. Для того, чтобы приложения написанное на Core можно было легко протестировать будем успользовать следующее практики и Framework’и:

  • Dependency Inversion Principle;
  • Dependency Injection;
  • IoC Container;
  • Moq framework .

И так, Dependency Inversion - это один из приципов SOLID. Он служит для создания слабосвязанных сущностей. Которых, можно легко модифицировать при минимальных изменениях в коде. Не следует путать Dependency Inversion с Dependency Injection. Это разные вещи.

Далее. Dependency Injection - это внедрения зависимостей. Если Dependency Inversion - это прицип, то Dependency Injection - это одна из реализаций этого приципа.

Существует два вида внедрения зависимостей:

  • Внедрения через конструктор;
  • Внедрения через свойства.

Внедрения через свойства можно реализовать через атрибуты [Dependency] в Unity, но как по мне лучше реализовывать через конструктор. Тогда объект становится более безопасным, в случае отказа от IoC Container’a. Так, что в рамках этого поста я буду использовать внедрения через конструктор.

IoC Container - это сущность на которой “завязываются” все узелки, именно этот класс и берет на себя “грех” создания объектов. Так же, как и Dependency Injection, IoC Container - это реализация приципов Dependency Inversion. IoC Container похож чем том на Абстрактную фабрику, только он берет на себя ещё обязанности нахождения зависимостей для каждого создаваемого объекта.

Для тестирования я буду использовать встроенный в ASP.NET Core IoC Container.

Moq framework - это framework, который позволяет имитировать объект и/или его функциональность. Такой объект называется moq-объект.

Что можно тестировать в ASP.NET Core MVC?

Да всё очень просто. Для начало можно тестировать View, или лучше сказать Frontend, который зачастую представлен в виде приложения. Можно также тестировать Model, бизнес логику, и Controller. Тестирования Frontend’a - это отдельная тема для поста. Сейчас же я буду тестировать Model и Controller.

Для начала надо создать слабо связанную архитектуру приложения.

Первым делом я создам интерфейс для репозитория. Репизиторием его можно назвать условно. Конечно, настоящий репозиторий имеет архитектуру гораздно сложнее:

 public interface IHomeRepository
{
        List<string> GetProducts();
}

Мой репозиторий будет возвращать список продуктов.

Теперь нужна релизация для моего репизитория. Релизация будет возвращать список продуктов, для простоты я буду создавать продукты прямо в методе:

public class HomeRepository : IHomeRepository
{
    public List<string> GetProducts()
    {
        List<string> products = new List<string>();
        products.Add("iPhone Xs");
        products.Add("Pixel 3");
        products.Add("Surface Pro 6");

        return products;
    }
}

Теперь я создам модель, которая будет использовать данный репозиторий. Но я не буду создавать просто реализацию, т.к. я хочу показать, что можно (и нужно) использовать слабосвязанные сущности, то я создам интерфейс, который будет представлять мою модель, а потом реализацию.

Для начала код интерфейса:

 public interface IHome 
 {
    List<string> GetProducts();
 }

Сама реализация:

public class HomeModel : IHome
{
    private readonly IHomeRepository _repository;

    public HomeModel(IHomeRepository repository)
    {
        _repository = repository ?? throw new OperationCanceledException("ctor: IHomeRepository can't be a null");
    }

    public List<string> GetProducts()
    {
         return _repository.GetProducts();
    }
}

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

Ну что, осталось внедрить зависимость в контроллер:

public HomeController(IHome home)
{
       _home = home ?? throw new OperationCanceledException("ctor: IHome cant's be a null");
}

Осталось зарегистрировать объекты в контейнере. Я как писал выше, я буду использовать встроенный контейнер в ASP.NET Core. Вот сообственно и сама регистрация:

services.AddTransient<IHome, HomeModel>();
services.AddTransient<IHomeRepository, HomeRepository>();

Теперь надо настроить отображения и проверить работоспособность приложения.

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

Вжух-вжух и я реализовал предсталения

Запущу приложения, для того, чтобы проверить его работоспособность.

test-of-asp-net-core

Всё работает отлично.

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

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

test-of-asp-net-core

Именуют тесты обычно так: НаименованияКлассTest,а методы которые нужно протестировать именуют так: TestНаименованияМетода.

Например: есть HomeModel с методом GetProducts() соответсвенно тесты для неё TestHomeModel с методом TestGetProducts();

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

И так, для того, чтобы протестировать модель мне надо создать объект модели. А для этого мне нужно передать в конструтор репозиторий как параметр. Если в качестве параметра передовать репозиторий, который используется на “бою” то отсюда появятся два неприятных обстоятельства: во-первых, если репозиторий зависит от абстракции, которая в свою очередь зависит тоже от абстрации, то мне придется писать кода для создания всей это иерархии и лишь для того, чтобы протестировать один метод. Во-вторых, при такой реализации я не пойму где была ошибка в модели или в репозитории, или в зависимости.

Как раз здесь мне и потребуется Moq framework. Как было написано выше с помощью moq’a можно имитировать объект.

Ок, погнали.

Для начало нужно подключить Moq framework к проекту с тестами. Это делается очень просто с помощью nuget.

Далее я создал в конструторе List, который будет я буду позиционировать как правильные данные:

private List<string> _products;

public HomeTest()
{
   _products = new List<string>();
   _products.Add("iPhone Xs");
   _products.Add("Pixel 3");
   _products.Add("Surface Pro 6");
}

При написаниях настоящих тестов такая реализация не допускается, но как говорится: “для примера сойдет”.

И так, а теперь самое интересно реализация метода для тестирования:

[Fact]
public void TestGetProducts()
{
    Mock<IHomeRepository> mockRepository = new Mock<IHomeRepository>();
    mockRepository.Setup(m => m.GetProducts()).Returns(new List<string>
    {
       "iPhone Xs",
       "Pixel 3",
       "Surface Pro 6"
    });

    HomeModel home = new HomeModel(mockRepository.Object);
    Assert.True(home.GetProducts().GetEnumerator().MoveNext());

    string surface = home.GetProducts()[2];
    Assert.Equal(_products[2], surface);
}

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

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

Теперь я покажу реализацию метода для тестирования контролера:

[Fact]
public void TestIndex()
{
    Mock<IHome> mockModel = new Mock<IHome>();
    mockModel.Setup(m => m.GetProducts()).Returns(new List<string>
    {
       "iPhone Xs",
       "Pixel 3",
       "Surface Pro 6"
    });

    HomeController controller = new HomeController(mockModel.Object);
    var indexResult = controller.Index() as ViewResult;
    string pixel = ((List<string>)indexResult.Model)[1];
    Assert.Equal(_products[1], pixel);
}

Здесь происходит примерно тоже самое, что в предыдущем тесте. Был создан moq-объект для IHome интерфейса. Далее я вытащил модель из контроллера и проверил возвращаемые данные.

Тестирование это неотлемлемая часть написания кода. Оно помогает находить ошибки быстрей и исправлять их дешевле. Но для того, чтобы покрыть тестированием большую часть кода нужно принимать различные приемы, паттерны и framework’и, которые были описаны в данном посте.

А закончить я хочу словами С. Макконнелла “…лучшем способом повышения производительности труда программистов и качества ПО является минимазация времени, затрачиваемого на исправления кода…”.

Подробнее

ASP.NET + SignalR = ❤

На конец-то я закончил писать пост про ASP.NET. Всё никак не могу выбрать тему для написания на данной технологии, но после долгих раздумей нашел наконец-то то, что искал. И это SignalR.

И так по порядку.

Во-первых, что такое SignalR.

SignalR - это абстракция над абстракцией, которая позволяет создавать динамический контент с использованием веб-технологий (и не только). Данная технология позволяет удаленно вызывать JS кода на стороне клиента. До того, как я узнал про SignalR я использовал для этой задачи WebSocket’ы. Так в чем главное отличие SignalR’а от Web Socket’ов?

А отличие состоит в том, что Signal может использовать WebSocket’ы как транспорт. А также SignalR может использовать в качества транспорта:

  • WebSocket;
  • EventSource;
  • Forever Frame;
  • Ajax long polling.

Да, можно на прямую создавать приложения с ипользованием WebSocket, но … зачем? Если можно перейти на ещё один уровень абстрации и избавиться от всех лишних действий.

Для того, чтобы показать как работает SignalR я создам Чат ha, classic.

Для этого нужно создать проект, как на картинке

asp-net-signalr-love

Сделаем наш чат немного по серьезней и добавим туда базу для пользователей. Для этого я буду использовать ASP.NET Indentity. Скажу честно не с первого раз у меня получилось “стартануть” Indentity.

Первым делом нужно добавить в проект следующее сборки:

  • Microsoft.AspNet.Identity.EntityFramework;
  • Microsoft.AspNet.Identity.OWIN;
  • Microsoft.Owin.Host.SystemWeb.

Далее нужно обновить Web.config, добавим туда следующее строчку:

<connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\aspnet-WebApplication6-20180724080537.mdf;Initial Catalog=aspnet-WebApplication6-20180724080537;Integrated Security=True" providerName="System.Data.SqlClient" />
  </connectionStrings>

P.S. Названия для подключения я выбрал весьма оригинальное.

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

Далее нужно создать наследника для IdentityUser, я назвал его ApplicationUser:

public class ApplicationUser : IdentityUser
{
    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
    {
        var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
        return userIdentity;
    }
}

Далее нужно добавить пользовательский контекс:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext()
        : base("DefaultConnection", throwIfV1Schema: false)
    {

    }

    public static ApplicationDbContext Create()
    {
        return new ApplicationDbContext();
    }
}

Теперь нужно создать две сущестности: первая будет отвечать за авторизацию, а вторая за регистрацию:

public class LoginModel
{
    public string Email { get; set; }

    [Required(ErrorMessage ="Поле должно быть задано")]
    public string Password { get; set; }
}

Первая сущность будет называться LoginModel. Данный класс будет содержать всего два свойства. При этом Password должен быть обязательным (как-будто без email’a кто-нибудь сможет зарегистрироваться ^_^).

Вторая сущность будет называться RegisterModel, а этот класс будет содержать уже целых три поля:

public class RegisterModel
{
    public string Email { get; set; }

        public string Password { get; set; }

    [Compare("Password", ErrorMessage ="Пароли не совпадают")]
    public string ConfirmPassword { get; set; }
}

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

Теперь осталось создать два специализированных класса. Первый класс будет отвечать за авторизацию и регистрацию, а второй будет отвечать за антентификацию.

Для этого в папке App_Start создадим класс с именем IndentityConfig.cs

asp-net-signalr-love

В нем создадим два класса (да-да, я знаю что в одном файле не рекомендуют создавать по несколько классов, но для примера можно и создать).

Первый класс будет называться ApplicationUserManager:

public class ApplicationUserManager : UserManager<ApplicationUser>
{
    public ApplicationUserManager(IUserStore<ApplicationUser> store)
        : base(store)
    {

    }

    public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
    {
        var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
        // Настройка логики проверки имен пользователей
        manager.UserValidator = new UserValidator<ApplicationUser>(manager)
        {
            AllowOnlyAlphanumericUserNames = false,
            RequireUniqueEmail = true
        };

        // Настройка логики проверки паролей
        manager.PasswordValidator = new PasswordValidator
        {
            RequiredLength = 6
        };

        // Настройка параметров блокировки по умолчанию
        manager.UserLockoutEnabledByDefault = true;
        manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
        manager.MaxFailedAccessAttemptsBeforeLockout = 5;

        return manager;
    }
}

Ну здесь вроде все понятно, а если не понятно то: создаем менеджер пользователей, задаем валидатор для проверки пользователей, задаем валидатор для проверки паролей и задаем найстроки блокировки по умолчанию. У UserValidator и PasswordValidator есть множество настройек, с которыми можно “поиграться” для получения нужного эффекта.

Далее создаем ApplicationSignManager:

public class ApplicationSignManager : SignInManager<ApplicationUser, string>
{
    public ApplicationSignManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager)
        :base(userManager, authenticationManager)
    {

    }

    public static ApplicationSignManager Create(IdentityFactoryOptions<ApplicationSignManager> options, IOwinContext context)
    {
        return new ApplicationSignManager(context.GetUserManager<ApplicationUserManager>(), context.Authentication);
    }
}

Не буду вдоваться в подробности, скажу что этот класс нужен для входа в наш будующий чат.

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

Так как мы использовали OWIN то заходим в Startup.cs редактируем его следующим образом:

public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.MapSignalR();
        ConfigureAuth(app);
    }
}

Во-первых, я его сделал partial. Это я сделал, для того, чтобы вынесни логику для настройки классов созданных выше. Логика настройки:

public partial class Startup
{
    public void ConfigureAuth(IAppBuilder app)
    {
        app.CreatePerOwinContext<ApplicationDbContext>(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
        app.CreatePerOwinContext<ApplicationSignManager>(ApplicationSignManager.Create);

        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login"),
            Provider = new CookieAuthenticationProvider
            {
                // Позволяет приложению проверять метку безопасности при входе пользователя. 
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                   validateInterval: TimeSpan.FromMinutes(30),
                   regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
            }
        });
        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
    }
}

Предворительная работа была сделана. Теперь можно созадть и контроллеры. Я создам два контроллера: HomeController, AccountController.

HomeController будет отвечать за отображения главной страницы чата: добавления сообщений и новых пользователей.

А вот работа с AccountController будет по интересней. Для начало создадим поля, которые будут отвечать за управления пользователями: входом и аутентификацией.

Поля:

private ApplicationUserManager _userManager;
private ApplicationSignManager _signManager;
private IAuthenticationManager _authenticationManager;

Далее создадом действия Login. На Login будет открываться страница авторизация.

Само действия:

[HttpGet]
public ActionResult Login()
{
    if (User.Identity.IsAuthenticated)
    {
        return Redirect("/Home/Index");
    }
    else
    {
        return View();
    }
}

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

Теперь время пришло, для того, чтобы добавить View-ку.

Здесь ничего особенного. Я буду использовать стандартные Boostrap-компоненты.

Исходный код View Login:

@model SignalChat.Models.Account.LoginModel

@{
    ViewBag.Title = "Login";
}

@Styles.Render("~/Content/Account/")

<img id="logo" src="~/Content/Images/ToxLogo.png" />

<br />

@using (Html.BeginForm("Login", "Account", FormMethod.Post, new { @class = "login-form", role = "form" }))
{
    @Html.AntiForgeryToken()
    <br/>
    <h4 style="text-align:center;">Войдите</h4>
    <br />
    <div class="form-group">
        @Html.LabelFor(m => m.Email, "Email", new { @class = "col-md-2 control-label" })
        <div>
            @Html.TextBoxFor(m => m.Email, new { @class = "form-control", style="width:350px;margin-left:13px;"})
        </div>
    </div>

    <br />
    <div class="form-group">
        @Html.LabelFor(m => m.Password, "Пароль", new { @class = "col-md-2 control-label" })
        <div>
            @Html.PasswordFor(m => m.Password, new { @class = "form-control", style = "width:350px;margin-left:13px;" })
        </div>
    </div>

    <div class="row">
        <input type="submit" class="col-md-3 btn btn-success" style="margin-left:28px;" value="Войти" />
        @Html.ActionLink("Регистрация", "Register", "Account", new { @class = "col-md-3 act-link", style= "float:right;margin-right:38px; margin-top:10px;" })
    </div>
}

Давайте разберемся, что я здесь написал. Для начало я указал в качестве модели для View-ки класс LoginModel(y нас почти все View-ки будут строго типизированные ^_^). Далее создаю форму, которая шлет запрос на контроллер (тот самый POST запрос, для которого я ещё не неписал действия).

Интересным момент здесь является эта строчка:

@Html.ActionLink("Регистрация", "Register", "Account", new { @class = "col-md-3 act-link", style= "float:right;margin-right:38px; margin-top:10px;" })

Здесь я делаю редирект на другое действия, которое отвечает за регистрацию.

Ну и вот что у нас получилось в итоге:

asp-net-signalr-love

Теперь давайте посмотрим на действия контроллера, которое проверяет валидность пользователя. Тот самый Login c фильтром POST:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginModel account)
{
    if (!ModelState.IsValid)
    {
        return View(account);
    }

    Inint();

    var result = await _signManager.PasswordSignInAsync(account.Email, account.Password, false, false);

    switch (result)
    {
        case SignInStatus.Success:
            return Redirect("/Home/Index");
        default:
            ModelState.AddModelError("", "Не удачная попытка входа");
            return View();
    }
}

Что здесь происходит? Я проверяю валидность модели, инцилизурую служубные классы (метод Init) и пытауюсь зайти под пользователем.

Код метода Init:

private void Inint()
{
    _userManager = HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
    _signManager = HttpContext.GetOwinContext().Get<ApplicationSignManager>();
    _authenticationManager = HttpContext.GetOwinContext().Authentication;
}

В зависимости от результата я перенаправляю пользователя или вывожу ошибку.

Так же нужно сделать View-ку для регистрации пользоватлей. Ну я уже её сделал:

@model SignalChat.Models.Account.RegisterModel
@{
    ViewBag.Title = "Регистрация";
}

@Styles.Render("~/Content/Account/")

<img id="logo" src="~/Content/Images/ToxLogo.png" />

<br />

@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "register-form", role = "form" }))
{
    @Html.AntiForgeryToken()
    <br />
    <h4 style="text-align:center;">Регистрация</h4>
    <br />
    <div class="form-group">
        @Html.LabelFor(m => m.Email, "Email", new { @class = "col-md-2 control-label" })
        <div>
            @Html.TextBoxFor(m => m.Email, new { @class = "form-control", style = "width:350px;margin-left:13px;" })
        </div>
    </div>
    <br />
    <div class="form-group">
        @Html.LabelFor(m => m.Password, "Пароль", new { @class = "col-md-2 control-label" })
        <div>
            @Html.PasswordFor(m => m.Password, new { @class = "form-control", style = "width:350px;margin-left:13px;" })
        </div>
    </div>
    <br />
    <div class="form-group">
        @Html.LabelFor(m => m.Password, "Повторите Пароль", new { @class = "col-md-2 control-label" })
        <div>
            @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control", style = "width:350px;margin-left:13px;" })
        </div>
    </div>
    <div class="form-group col-md-10">
        <input type="submit" class="col-md-3 btn btn-success" style="width:170px;" value="Зарегистрироваться" />
        @Html.ActionLink("Войти", "Login", "Account", new { @class = "col-md-3 act-link", style = "float:right;margin-right:-65px;margin-top:10px;" })
    </div>

}

По большей части это такая же View-ка, что и для входа, но только в качестве модели используется RegisterModel:

asp-net-signalr-love

По большому счету я все сделал. Осталось только реализовать сам чат.

И так вот и View-ка чата:

@{
    ViewBag.Title = "Chat";
}

@Scripts.Render("~/bundles/home/")
@Styles.Render("~/Content/Home/")
<script src="~/signalr/hubs"></script>

<section class="stn-root">
    <div class="row" style="background:#6bc260;height:5%;width:1000px;margin-left:0px;">
        <div class="col-md-offset-10">
            <img src="https://mem.gfx.ms/me/MeControl/9.18199.0/msa_enabled.png" style="height: 30px; width: 30px; display:inline-block;">
            <span id="span-user" style="color:white;display:inline-block;margin-top:8px;margin-left:10px;">@ViewBag.User</span>
        </div>
    </div>
    <div class="row" style="height:100%;width:1000px;margin-left:0px;background-color:#FCFCFC;">
        <div id="div-users" class="col-sm-4" style="height:100%;overflow:auto;border-right:1px solid;">
        </div>
        <div class="col-sm-8" style="height:100%;">
            <div id="div-messages" style="height:87%;overflow:auto;">
            </div>
            <div class="row" style="height:7%;">
                <div class="col-md-10">
                    <input id="input-message" type="text" class="form-control" placeholder="Введите сообщения" />
                </div>
                <div class="col-md-1" style="margin-left:-14px;">
                    <button class="btn btn-success" onclick="onSendMessage()">Отправить</button>
                </div>
            </div>
        </div>
    </div>

</section>

Вот что получилось:

asp-net-signalr-love

И так что здесь интересного? Во-первых, это Bundles. Вот код для bundles:

public static void RegisterBundles(BundleCollection bundles)
{
    bundles.Add(new ScriptBundle("~/bundles/home/").Include(
        "~/Scripts/jquery-{version}.js",
        "~/Scripts/Home/index.js",
        "~/Scripts/jquery.signalR-2.3.0.js"
    ));

    bundles.Add(new StyleBundle("~/Content/Account/").Include(
        "~/Content/bootstrap.css",
        "~/Content/Account/Account.css"));

    bundles.Add(new StyleBundle("~/Content/Home/").Include(
        "~/Content/bootstrap.css",
        "~/Content/Home/Index.css"));
}

Во-вторых, логика для добавления новых сообщений будет осуществлятся на клиенте.

Для начала создам js-скрип в нем создам метод, который будет подписываться на hub’ы:

function workWithHub() {
    var chat = $.connection.chatHub;
    chat.client.sendMessages = function (userName, text) {
        addMessage(userName, text);
    };
    chat.client.addUser = function (userName) {
        addNewUser(userName);
    };
    $.connection.hub.start().done(function () {
        var userName = document.getElementById('span-user').textContent;
        chat.server.login(userName);
    });
}

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

Функции addMessage() и addNewUser():

function addMessage(userName, text) {
    var newMessage = getOtherMessage(text, userName);
    $('#div-messages').append(newMessage);
    document.getElementById('input-message').value = '';
}
function addNewUser(user) {
    var newUser = getUser(user);
    $('#div-users').append(newUser);
}

Ну и вспомогательные методы для создания html разметки:

function getOtherMessage(text, name) {
    return '<div style="background:#f3f3f3;display:block; border-radius:5px;margin-top:15px;margin-right:10px;">'
        + '<span style = "font-size:12px; margin-left:5px;">' + name + '</span>'
        + '<br /><span style="margin-left:5px;text-wrap:normal">' + text + '</span></div>';
}

function getUser(userName) {
    return '<div style="border-bottom:1px solid;border-top:1px solid; margin-top:5px;">'
        + '<img src = "https://mem.gfx.ms/me/MeControl/9.18199.0/msa_enabled.png" style = "height: 30px; width: 30px; display:inline-block;">'
        + '<span style="display:inline-block;margin-top:8px;margin-left:10px;">' + userName + '</span></div>';
}

Теперь нам надо реализовать функцию при нажатие на кнопку “Отправить”:

function onSendMessage() {
    var userName = document.getElementById('span-user').textContent;
    var text = document.getElementById('input-message').value;
    var chat = $.connection.chatHub;
    chat.server.send(userName, text);
    var newMessage = getMyMessage(text, userName);
    $('#div-messages').append(newMessage);
    document.getElementById('input-message').value = '';
}

И вспомогательная функция для отправки своих сообщений:

function getMyMessage(text, name) {
    return '<div style="background:#E6F0FA;display:block; border-radius:5px;margin-top:15px;margin-right:10px;">'
        + '<span style = "font-size:12px; margin-left:5px;">' + name + '</span>'
        + '<br /><span style="margin-left:5px;">' + text + '</span></div>';
}

Побольшому счету вот и всё.

Посмотрим, что у нас получилось.

asp-net-signalr-love

Вот в приципе и всё. Получилось много буков, но всё же давольно интересная тема для изучения.

Подробнее

PiggyBank

Этот пост будет обзорным. Здесь не будет ни строчки кода. В этом посте я расскажу как создал свое первое приложение.

Почему мне в голову пришла мысль о написании приложения? Дело в том, что идея контролировать личный бюждет у меня возникла давно. Раньше я обходился таблицей в Excel’e, но потом я сказал себе: “Я же программист, почему я не могу написать приложение для этого?”.

Ну с января этого года начал делать какие-то наброски. В первую очередь выбрал метод, который называю “сверху-вниз”. Идея данного метода заключается в следующем: сперва рисуешь интерфейс, а затем пишешь логику.

В первых набросках не придерживался никакого Fluent Design’a потому, что хотел чтобы приложение запускаскалось на телефоне (да, да я использую Windows 10 Mobile), а последняя версия Windows на телефонах не поддерживает данную концепцию дизайна.

Но всё изменилось когда был подключен EF для хранения БД. Дело в том, что Microsoft активно развивает кроссплатформенный .Net Core, и , соответственно, есть EF 6 и EF Core. Я не хотел выбирать EF 6 потому, что в скором будущем остановиться поддержка данного framework’a, и так активно он не будет развиваться как EF Core.

После подключения EF Core приложения не запустилось бы на телефоне. Поэтому было принято решения переработать дизайн в соответсвии с Fluent Design.И вот что получилось.

piggybank

piggybank

piggybank

piggybank

piggybank

Но при написании приложения у меня возникла интересная ситуация. Подгружать все элементы на страницу “Затраты” нерационально, поэтому я подгружаю первые десять, а если пользователю покажется, что первой страницы ему малова-то то он в любой момент может подгрузить следующею страницу нажам на кнопку “Refresh”. Для этой ситуации было решено использовать контрол RefreshContainer. Но меня ждало разочирование потому, что в минимальной версии (Windows 10 1709), которая была выбрана, данного контейнера нет. Но при этом в версии Windows 10 1703 данный контрол есть o_0. И тут я осознал всю бренность существования. Пришлось увеличить минимальную версию до 1803. Но на этом сюрпризы не закончились. После обновления минимальной версии и обновилось и UWP SDK. Соответсвено, обновились и некоторые элементы.

И после очердного ребилда я заметил, что у меня две стрелки назад WHF?. И тут сразу вспомнил о чем говорилось на Build 2018 и убрал одну из стрелок назад. Это связано с тем, что в гредущем обновлении добавится новая функция “Sets”, которая займет заголовок окна и использовать кнопку назад в заголовке станет невозможно. И Microsoft обновила контрол NavigationView добавив туда программную кнопку назад. Что в моем случае (т.к. я использовал данный контрол) сыграла на руку: во-первых, это потребывало минимальных изменений в коде, во-вторых, моё приложение готово к гредущему обновлению.

Исходный код моего приложения лежит на GitHub’e. Если есть идеи, то you are welcome!

Скачать приложение можно тут

Подробнее

Full version of MasterViewControl

После релиза UWP Microsoft показала как должны выглядить современные приложения на Windows 10. И так вопрос: что должно быть у приложения, чтобы оно соответствовало универсальной платформе? Канечно же адаптивный пользовательский интерфейс, так как UWP может запускаться на различных устройствах, с различной диагональю экрана (или без).

one_platform

Например, для рассмотрения я взял стандартный клиент для почты. У него имеется два состояния (на самом деле у данного клиента есть три состояния, но для моего примера важны только два):

“Narrow”

standart_client_state_1

“Wide”

standart_client_state_2

При этом, в разных состояниях меняется и UX мастер UI/UX:

“Narrow”

standart_client_state_2

“Wide”

standart_client_state_2

Покопаясь в интернете я нашел, что-то… А точнее описание такого паттерна поведения GUI, который называется Master/details pattern.

master_details_pattern

Т.е. есть уже готовое решение, которое можно использовать для написания приложения на UWP. Нужно всего лишь добавить шаблон, пару стилей и “вуаля” - всё готово. Но не всё так просто, как казалось на первый взгляд. Первом делом я расмотрел то, что предлагает Microsoft. А именно Master/details sample.

Дальше решение от Microsoft. Слабонервных попрошу уйти.

<Grid x:Name="LayoutRoot" Loaded="LayoutRoot_Loaded">
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="AdaptiveStates" CurrentStateChanged="AdaptiveStates_CurrentStateChanged">
            <VisualState x:Name="DefaultState">
                <VisualState.StateTriggers>
                    <AdaptiveTrigger MinWindowWidth="720" />
                </VisualState.StateTriggers>
            </VisualState>

        <VisualState x:Name="NarrowState">
            <VisualState.StateTriggers>
                   <AdaptiveTrigger MinWindowWidth="0" />
            </VisualState.StateTriggers>

            <VisualState.Setters>
                <Setter Target="MasterColumn.Width" Value="*" />
                <Setter Target="DetailColumn.Width" Value="0" />
                <Setter Target="MasterListView.SelectionMode" Value="None" />
            </VisualState.Setters>
        </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>

        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition x:Name="MasterColumn" Width="320" />
            <ColumnDefinition x:Name="DetailColumn" Width="*" />
        </Grid.ColumnDefinitions>

        <TextBlock
            Text="My Items"
            Margin="12,8,8,8"
            Style="{ThemeResource TitleTextBlockStyle}" />

        <ListView .../>

        <ContentPresenter .../>
</Grid>

Теперь можно выдохнуть.

Давайте поставим все точки над i. Что нужно делать-то? Взять Grid. Разбить этот Grid на несколько колонок. В нем определить VisualStateManager, которые будут реагировать на изменения размера. И в случае, когда ширина Grid’a будет меньше определенного размера, “захлопнуть” колонку с DetailView и показать DetailViewPage.

Почему это решение нельзя назвать полноценным MasterDetailView?

Во-первых, ContentPresenter’y был задан определенный шаблон, и из-за этого в DetailView можно поместить только элемент из ListView. Следовательно, можно забыть об использовании CommandBar’a в MasterView. Во-вторых, так как ContentPresenter представляет из себя обычный контейнер для отображения содержимого ListView, то можно забыть о навигации внутри DetailView (как это реализовано в стандартном почтовом клиенте). Microsoft как-бы сказали:”Вот так вот должно выглядить приложение. Вот такой паттерн нужно использовать. Но готового контрола для реализации этого паттерна в приложениях у нас нет, но вы можете его сделать сами! Никто Вам не мешает.”

Ок. Я так и сделаю.

Что мне нужно от full MasterDetailView?

Во-первых, у данного представления должно быть два рабочих состояния, как у стандартного клиента для почты. Во-вторых, в данном представлении должны отсутсвовать недочеты, которые есть в примере от Microsoft.

Может кто-нибудь уже написал что-то похожее? Откроем исходники Unigram потомучто!. Поведения приложения Unigram меня устраевает, но их решения слишком неоправданно сложное (кому интересно, может посмотреть в исходники). Разработчики данного приложения используют NavigationService, что для приложения с нестрогой MVVM архитектурой необязательно.

Ну что же? Раз решил давай напиши.

Мне нужен контрол, состоящий из двух частей: MasterView и DetailView. При этом в MastrView’e должен находится не только ListView, а любой элемент, который разработчик захочет. А DatailView должен содержать Frame, чтобы можно осуществлять навигацию по страницам. Для начала возьмем и создадим класс, который назавем, ну например, MasterDetailView и который наследуется от ContentControl. Почему наш класс должен наследоваться от ContentControl? В MasterDetailView у нас будет ContentPresenter. А как говорил один легендарный .net разработчик: “Лучший контейнер для ContentPresenter’a - это ContentControl”.

В конструкторе я пропишу следующее:

public MasterDetailView()
{
    DefaultStyleKey = typeof(MasterDetailView);

    Loaded += OnLoaded;
    Unloaded += OnUnloaded;
}

Для чего нам нужны обработчики для событий Loaded и Unloaded? В этих обработчиках я буду подписываться (и отписываться) на события кнопки Back, примерно так.

SystemNavigationManager.GetForCurrentView().BackRequested += OnBackRequested;

В методе OnBackRequested() я буду переходить на предыдущую страницу. Что нам ещё нужно? Мне нужно, чтобы внешний пользователь,тот кто использует мой контрол мог узнать текущие состоние. Для этого создадим обычный enum, который характеризует эти состояния.

public enum MasterDetailVisualState : byte
{
    Narrow,
    Wide
}

А в классе MasterDetailView будет свойство CurrentState, которое показывает текущее состояние. Теперь нам надо менять свойства CurrentState при изменении в VisualStateGroup. Для этого переопределим метод OnApplyTemplate() и я напишу в нем следующее.

protected override void OnApplyTemplate()
{
    _masterPresenter = (ContentPresenter)GetTemplateChild("MasterPresenter");
    _detailPresenter = (Frame)GetTemplateChild("DetailPresenter");
    _stateGroup = (VisualStateGroup)GetTemplateChild("AdaptiveStates");
    
    _stateGroup.CurrentStateChanged += OnCurrentStateChanged;
    CurrentState = _stateGroup.CurrentState.Name == "WideState" ? MasterDetailVisualState.Wide :
    MasterDetailVisualState.Narrow;
}

Здесь я подписываюсь на событие CurrentStateChanged у VisualStateGroup. Данное событие возникает, когда происходит изменение состония. В методе OnCurrentStateChanged() меняю значение у свойства CurrentState. Его реализация

private void OnCurrentStateChanged(object sender, VisualStateChangedEventArgs e)
{
    if(e.NewState.Name == "WideState")
    {
        CurrentState = MasterDetailVisualState.Wide;
    }

    if(e.NewState.Name == "NarrowState")
    {
        CurrentState = MasterDetailVisualState.Narrow;
    }
    
    UpdateView();
    ViewStateChanged?.Invoke(this, EventArgs.Empty);
}

Далее нужно реализовать метод, который бы изменял видимость у _masterPserenter и _detailPresenter.
В нем не будет ничего сложного. Назову-ка я его UpdateView().

private void UpdateView()
{
    if (CurrentState == MasterDetailVisualState.Filled)
    {
        _masterPresenter.Visibility = Visibility.Visible;
        _detailPresenter.Visibility = Visibility.Visible;
    }
    
    if (CurrentState == MasterDetailVisualState.Narrow && _detailFrame.Content == null)
    {
        _masterPresenter.Visibility = Visibility.Visible;
        _detailPresenter.Visibility = Visibility.Collapsed;
    }
    
    if (CurrentState == MasterDetailVisualState.Filled && _detailFrame.Content != null)
    {
        _masterPresenter.Visibility = Visibility.Collapsed;
        _detailPresenter.Visibility = Visibility.Visible;
    }
}

Ой! Чуть не забыл показать реализацию метода OnBackRequested().

private void OnBackRequested(object sender, BackRequestedEventArgs e)
{
    if (CurrentState == MasterDetailVisualState.Filled)
    {
        if (_detailFrame.CanGoBack)
        {
            _detailFrame.GoBack();
        }
    }
    else
    {
        if (_detailFrame.BackStack.Count > 1)
        {
            if (_detailFrame.CanGoBack)
            {
                _detailFrame.GoBack();
            }
        }
        else
        {
            _detailPresenter.Visibility = Visibility.Collapsed;
            _masterPresenter.Visibility = Visibility.Visible;
            _detailFrame.BackStack.Clear();
        }
    }
}

Теперь пожалуй один из главных моментов, так как у меня не используется NavigationService, то моему контролу нужно как-то переходить на другие страницы. Для это я создам в классе MasterDetailView метод Navigate(), который принимал был тип страницы и параметры, которые необходимы для перехода.

public void Navigate(Type typePage, object args)
{
    UpdateView();
    _detailFrame.Navigate(typePage, args);
}

Этот метод не идеален, но для примера сойдет. Теперь нужно определить шаблона для моего контрола. Определю-ка я его в нутри стиля.

<Style TargetType="Controls:MasterDetailView">
        <Setter Property="BorderBrush" Value="{ThemeResource SystemControlForegroundBaseLowBrush}"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Controls:MasterDetailView">
                    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="AdaptiveStates">
                                <VisualState x:Name="WideState">
                                    <VisualState.StateTriggers>
                                        <AdaptiveTrigger MinWindowWidth="820"/>
                                    </VisualState.StateTriggers>
                                    <VisualState.Setters>
                                        <Setter Target="MasterColumn.Width" Value="260*" />
                                        <Setter Target="DetailColumn.Width" Value="540*" />
                                        <Setter Target="MasterColumn.MinWidth" Value="72" />
                                        <Setter Target="MasterColumn.MaxWidth" Value="540" />
                                        <Setter Target="DetailPresenter.(Grid.Column)" Value="1"/>
                                    </VisualState.Setters>
                                </VisualState>
                                <VisualState x:Name="NarrowState">
                                    <VisualState.StateTriggers>
                                        <AdaptiveTrigger MinWindowWidth="0"/>
                                    </VisualState.StateTriggers>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition x:Name="MasterColumn"/>
                            <ColumnDefinition x:Name="DetailColumn" Width="0"/>
                        </Grid.ColumnDefinitions>
                        <ContentPresenter x:Name="MasterPresenter"
                                          Content="{TemplateBinding Content}"
                                          ContentTemplate="{TemplateBinding ContentTemplate}"
                                          ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}"
                                          ContentTransitions="{TemplateBinding ContentTransitions}"
                                          DataContext="{TemplateBinding DataContext}"
                                          HorizontalContentAlignment="Stretch"
                                          VerticalContentAlignment="Stretch"/>

                        <Frame x:Name="DetailPresenter" Background="{TemplateBinding Background}">
                            <TextBlock Text="Ничего не выбрано, пожалуйста выберите" HorizontalAlignment="Center"  VerticalAlignment="Center"/>
                        </Frame>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
</Style>

Давай-те посмотрим на шаблон по внимательней. Что я изменил?

Во-первых, в качестве MasterView выступает ContentPresenter. А это означает, что в нем можно поместить всё что угодно. Во-вторых, в качестве DetailView выступает Frame (да здравствует навигация без NavigationService).

Итог:

“Narrow”

full_masterdetailview

“Wide”

full_masterdetailview

Чуть не забыл, я добавил капельку акрила.

Подробнее

Windows 10 IoT Core

Вот и наступил тот долгожданный день,когда ко мне пришел Resberry Pi 3. Заказывал я его из Китая и через пару недель он добрался до меня. Зачем я его заказывал? Всё хотел посмотреть что за зверь этот Windows 10 IoT Core. После того, как Microsoft представила UWP и объявила о том, что приложения написанные на универсальной платформе могут запускаться на разных устройствах, в том числе и на IoT. Мне стало интересно и я захотел посмотреть и разобраться в том, как с этим работать.

My helpful screenshot

Первым делом я зашел на портал Microsoft, чтобы посмотреть с чего мне начать. Microsoft предлагали купить устройства, которые подерживали Windows 10 IoT Core. Так как этот пункт меня не интересовал, я его пропустил. Далее предлагали настроить устройства, но для начала мне нужна была SD-карта (SD card-based devices). Немножко покопаясь я нашел карты, которые рекомендовали Microsoft.

My helpful screenshot

Дело оставалось за малым, зашел в Ситилнк, заказал рекомендованную почти флешку (Рекомендованная: Samsung 32GB EVO Class 10 Micro SDHC. Заказал: Samsung 32GB EVO Pluse Class 10 Micro SDHC).

Я, с большой надеждой, начал записывать образ OC’и на флешку.

My helpful screenshot

Всё было хорошо, до появления сообщения об ошибке. Как наивный человек, я попробывал ещё 3 раза записать на флешку Windows 10 IoT Core и как итог: я получил только седую бороду. Делать было нечего нашел старую флешку на 16 Гбайт 4 Class и попробывал с ней…Записалось WTF Microsoft?

My helpful screnshot

После чего я собрал свой Rasberry Pi 3, подключил питание и начал ждать, пока мой rasiot (мой мини пк) отобразиться в IoT Dashboard’e. В инструкциии было сказано, что подключение к локальной сети может занять 15 минут. Это зависит от скрости чтения с флешки, а так как флешка, которую я использовал, была не самого хорошего класса, я подождал 40 минут, но так ничего не добился. После чего я подумал:” А что если rasiot не может подключится к Wi-Fi?”- и подключил его к локальной сети с помощью кабеля. Через пару секунд rasiot отобразился в Dashboard’e.

My helpful screenshot

Полдела сделано. Мне стало интересно, а как выглядит Windows 10 IoT Core и что с ней можно делать.

Я представил себе какой-нибудь плиточный интерфейс (CShel on all). Ну что же надо зайти на устройтва. Открыл Device Portal. Как ни странно догадаться от меня потребывали логин и пароль администратора. Пароль я вводил при настройке, а вот логин я не вводил. Пришлось немножко потыкаться в IoT Dashboard’e, чтобы найти “Имя пользователя”. Имя по умолчанию Администратор. Ввожу имя пользователя и пароль - ошибка. Ещё раз - ошибка. И тут я подумал: “Администратор имя по умолчанию для русской локализации, а у меня на rasiot’e , скорей всего, стоит английская локализация. А что если вдруг имя Администратор для rasiot - это Administrator”. И как ни странно я смог залогиниться. Windows 10 IoT Core для внешнего наблюдателя представляет из себя web-портал.А как же CShell on all?

My helpful screenshot

И так теперь можно запускать UWP приложение на нашем мини пк. Для этого запускаем Visual Studio. Создаем пустой UWP проект “SampleForRasIoT”. Ничего не будем изменять в бланке по умолчанию. Запустим отладку на “Удаленном компьютере”. При первом запуске отладки на удаленом копмптьютере Visual Studio попросит ввести IP адрес нашего устройства, на котором будет проходит удаленная отладка. Вводим IP rasiot. После того, как успешно был введен IP адрес, можно переходить в “Портал устройства” и посмотреть запущено ли наше приложение.

My helful screenshot

Как видно приложение работает нормально. Ну что же пустое приложение на мини пк вещь интересная, но бесполезная. Напишем-ка приложение, которое показывало бы текущую погоду. В качестве сервиса с погодой будем использовать OpenWeatherMap. Первое что надо сделать - это зарегистрироваться как разработчик. Это нужно для того, чтобы сервис OpenWeatherMap предоставил нам APIID для использования сервиса. Так как у нас rasiot может только отображать информацию, то реализуем одностраничное приложние, которое бы показывало текущую погоду на день. Добавим в проект “SampleForRasIoT”, на страницу MainPage.xaml, нужные элементы. Добавим заголовок, который будет отображать геолокацию (Nuzhniy Novgorod Forever), показание с “термометра”, ну и пиктограмму погоды.


            <!--Title-->
            <TextBlock x:Name="TitleTextBlock" Grid.Row="0"  HorizontalAlignment="Center" FontSize="48" TextWrapping="WrapWholeWords"/>
            <!--Temperature-->
            <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center">
                <Image x:Name="WeatherImage" Height="100" Width="100"/>
                <TextBlock x:Name="TempratureTextBlock" FontSize="100" VerticalAlignment="Center" Margin="0,0,0,25"/>
                <TextBlock Text="°C" FontSize="32" VerticalAlignment="Center" Margin="0,0,0,60"/>
            </StackPanel>

Теперь добавим данные на страничку. Для этого подпишемся на события Loaded страницы. Создадим метод. В этом методе создадим httpClient, который будет стучаться до openweathermap и брать от туда данные и задавать элементам на страничке.


        HttpClient httpClient = new HttpClient();
        var serverAnswer = await httpClient.GetStringAsync(URL + _queryParameter + _apiKey);

Так как данные к нам приходят в json-формате (формат можно выберать из: json, xml и html), то их нужно распарсить. Для этого добавим в проект Newtonsoft.Json через Nuget. Далее нужно преобразовать данные с сервера в модель для того, чтобы ими было легче манипулировать. Так как мне лень писать для response модель, то воспользуемся динамическими возможностями языка C#, т.е. ключивым словом dynamic.

        dynamic dynObj = JsonConvert.DeserializeObject(serverAnswer);

Ну чтоже теперь зададим значение для элементов управления.

        TitleTextBlock.Text = $"{dynObj.name}";
	WeatherImage.Source = new BitmapImage(new Uri(_http + _apiImg + $"{dynObj.weather[0].icon}.png"));
	TempratureTextBlock.Text = $"{dynObj.main.temp}";

Здесь нужно отметить, что “стучаться” за данными нужно на адрес: http://api.openweathermap.org/data/2.5/weather

А чтобы получить пиктограмму на другой адрес: http://openweathermap.org/img/w/

Теперь нужно проверить, что у нас получилось. Для этого запустим приложение.

My helpful screenshot

Конечно хорошо, что приложение показывает гелокацию и погоду, но для настоящего погодного приложения этого маловато. Сервис OpenWeatherMap ограничивает прогоноз погоды одним днем, но это не означает что я не могу вывести всю доступную информацию о погоде на день. Покапаясь в API openweathermap я понял какие данные можно получить от сервиса. В соответсвие с этим данными и был переработан интерфейс приложения (добавил пару элементов). Также я добавил автообновление данных каждые 10 минут.
Запускаем приложение на rasiot’e и смотрим, что получилось.

My helpful screenshot

Подробнее

Простой диалоговый сервис

Я был сильно удивлен, когда узнал что при создании WPF-проекта нужно будет создавать свой диалоговый сервис. Разработчику доступны диалоги только с основным функционалом: открыть, сохранить и т.д. Так для чего он вообще нужен-то? Он нужен для отображения модальных окон, отличающихся от стандартных. Для того, чтобы показать как можно сделать простой диалоговый сервис я создам простое WPF-приложения.

My helpful screenshot

Добавим в решение ещё один проект «Библиотеку классов».

My helpful screenshot

Приступаем к написанию сервиса. Для начало создадим интерфейс в проекте DialogService, который будет является супер типом для ViewModel’ей. Он будет пустой. Я его назову IViewModelModal. Затем я создаю интерфейс, который будет представлять диалоговый сервис.

namespace DialogService
{
    interface IDialog
    {
        void ShowModal(IViewModel viewModel, Window window);
    }
}

Далее нужно реализовать его, для этого я создам класс, который назову Dialog.

namespace DialogService
{
    public class Dialog : IDialog
    {
        public void ShowModal(IViewModelModal viewModel, Window window)
        {
            var vm = viewModel;
            var win = window;
            win.ShowInTaskbar = false;
            win.Height = window.Height;
            win.Width = window.Width;
            win.WindowsStyle = WindowStyle.ToolWindow;
            win.DataContext = vm;
            win.ShowDialog();
        }
    }
}

Ну всё. Теперь можно пойти и выпить чаю потому, что диалоговый сервис готов. Осталось дело за малым, показать как он работает. Для этого на главное окно добавим кнопку, которая и будет вызывать диалоговое окно. Так как диалоговый сервис предполагает использования паттерна MVVM то логичней было бы сделать привязку команды в ViewModel. Для этого создаем простой класс Command, который реализует ICommand.

public Class Command : ICommand
{
    readonly Action<object> _execute;

    public Command(Action<objet> execute)
    {
        _execute = execute;
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add {CommandManager.RequerySuggested += value;}
        remove {CommandManager.RequerySuggested -= value;}
    }
}

Далее нужно реализовать окно для диалога. Оно представляет из себя обычное окно, которое содержит TextBox по середине. Для этого создаем ModalViewModel, которая содержит одно текстовое свойства. Далее реализовываем MainViewModel. Данный класс будет содержать целую одну командищу.

public class MainViewModel
{
    private void ShowDialog(object obj)
    {
        var dialogService = new Dialog();
        dialogService.ShowModal(new Dialog(), new ModalViewModel());
    }

    public ICommand ShowDialogCommand
    {
        get
        {
            return new Command(ShowDialog);
        }
    }
}

После чего запускаем наш проект и смотрим, что же получилось.

My helpful screenshot

Подробнее

Почему начал вести свой блог?

Пожалуй начнем с того, для кого я веду этот блог. Как не странно, я его завел только для себя. Да да, для себя. Мой блог — это мой внутренний монолог, который я буду вести на просторах интернета. Так как я являюсь начинающим программистом, то мой блог будет посвящён программированию на C# и не только. Так же я хочу выражать мысли по поводу изменений в IT сфере и о новых «штучах». Как будет на деле, мы увидим позже.

Почему в качестве основы для своего блога я выбрал wordpress? Всё очень просто. Мне понравилось то, что предлагал wordpress. А он предлагает бесплатное размещение на своих хостах и возможность зарегистрировать домен 3-го уровня.Вот это я и посчитал оптимальным решением для блога.

Да этого я ни разу не работал на wordpress, но решил что для меня это будет не лишним опытом. И вот 17 ноября я зарегистрировал свой блог. С CMS я разобрался довольно быстро и приступил к оформлению блога. Так, как я знал что хочу видеть, то тему для оформления я нашел сразу же. Осталось от редактировать внешний вид и «вуаля» мой блог готов.

Но со временем, я понял что WordPress не совсем меня устраевает. Поэтому я перенс свой блок на GitHub.

Подробнее