Перейти к основному содержимому

Практикум WPF — клиент на Prism

Разработчику Архитектору

Практикум, шаг 4 из 6. Собираем TaskDesk.Client с Prism — DI, регионы и навигация поверх MVVM. ViewModel из шага 2; API из шага 3.


Зачем Prism поверх «голого» WPF

Prism (Prism Library, открытый проект на GitHub) добавляет к MVVM:

ВозможностьБез PrismС Prism
DI контейнерРучной new в App.xaml.csRegisterTypes, внедрение в VM
НавигацияСмена Content вручнуюIRegionManager, URI TaskListView
МодульностьОдин exeIModule, lazy-load (в крупных системах)
События между VMСвязанные ссылкиIEventAggregator

Для TaskDesk достаточно Prism.DryIoc (контейнер DryIoc в комплекте) и одного модуля с двумя View.


Пакеты и шаблон проекта

cd TaskDesk/src
dotnet new wpf -n TaskDesk.Client -o TaskDesk.Client
dotnet add TaskDesk.Client reference ../TaskDesk.Core
dotnet add TaskDesk.Client package Prism.DryIoc
dotnet add TaskDesk.Client package CommunityToolkit.Mvvm
dotnet add TaskDesk.Client package Microsoft.Extensions.Configuration.Json

Структура папок:

TaskDesk.Client/
├── App.xaml / App.xaml.cs → PrismApplication
├── Views/
│ ├── ShellWindow.xaml → оболочка с регионом
│ └── TaskListView.xaml
├── ViewModels/
│ ├── ShellViewModel.cs
│ └── TaskListViewModel.cs
├── Services/
│ └── ApiTaskRepository.cs
└── appsettings.json → BaseUrl API

PrismApplication — точка входа

App.xaml:

<prism:PrismApplication x:Class="TaskDesk.Client.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/">
<Application.Resources>
<!-- общие стили -->
</Application.Resources>
</prism:PrismApplication>

App.xaml.cs:

public partial class App : PrismApplication
{
protected override Window CreateShell() =>
Container.Resolve<ShellWindow>();

protected override void RegisterTypes(IContainerRegistry container)
{
container.RegisterSingleton<ITaskRepository, ApiTaskRepository>();
container.RegisterForNavigation<TaskListView, TaskListViewModel>();
}

protected override void OnInitialized()
{
base.OnInitialized();
var regionManager = Container.Resolve<IRegionManager>();
regionManager.RequestNavigate("ContentRegion", nameof(TaskListView));
}

protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
// один модуль — можно опустить IModule и держать всё в App
}
}

Shell — окно с регионом

ShellWindow.xaml:

<Window x:Class="TaskDesk.Client.Views.ShellWindow"
Title="TaskDesk" Height="520" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Padding="12" Background="#1e293b">
<TextBlock Text="TaskDesk" Foreground="White" FontSize="18"/>
</Border>
<ContentControl Grid.Row="1"
prism:RegionManager.RegionName="ContentRegion"/>
</Grid>
</Window>

Prism подставляет в ContentControl View, зарегистрированную через RegisterForNavigation. DataContext ViewModel создаётся автоматически и получает зависимости из DI.


ApiTaskRepository — HttpClient к API

appsettings.json:

{
"TaskDeskApi": {
"BaseUrl": "http://localhost:5100/"
}
}
public sealed class ApiTaskRepository : ITaskRepository
{
private readonly HttpClient _http;

public ApiTaskRepository(HttpClient http) => _http = http;

public async Task<IReadOnlyList<TaskItem>> GetAllAsync(CancellationToken ct = default)
{
var dtos = await _http.GetFromJsonAsync<List<TaskDto>>("api/v1/tasks", ct)
?? new List<TaskDto>();
return dtos.Select(Map).ToList();
}

public async Task<TaskItem> CreateAsync(TaskItem task, CancellationToken ct = default)
{
var response = await _http.PostAsJsonAsync(
"api/v1/tasks",
new CreateTaskRequest(task.Title, task.Status.ToString()),
ct);
response.EnsureSuccessStatusCode();
var dto = await response.Content.ReadFromJsonAsync<TaskDto>(cancellationToken: ct)
?? throw new InvalidOperationException("Empty body");
return Map(dto);
}

private static TaskItem Map(TaskDto dto) => new()
{
Id = dto.Id,
Title = dto.Title,
Status = Enum.Parse<TaskStatus>(dto.Status),
CreatedAt = dto.CreatedAt
};
}

Регистрация HTTP в RegisterTypes (упрощённый вариант для учебного проекта):

container.RegisterSingleton<ITaskRepository>(c =>
{
var config = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: true)
.Build();
var baseUrl = config["TaskDeskApi:BaseUrl"] ?? "http://localhost:5100/";
var http = new HttpClient { BaseAddress = new Uri(baseUrl) };
return new ApiTaskRepository(http);
});

Конструктор ApiTaskRepository в этом случае принимает HttpClient напрямую. В production-проекте предпочтительнее IHttpClientFactory через Microsoft.Extensions.Http и named client — см. DI в .NET.


ViewModel с загрузкой при навигации

Prism поддерживает IInitializable / INavigationAware:

public partial class TaskListViewModel : ObservableObject, INavigationAware
{
private readonly ITaskRepository _repository;

public TaskListViewModel(ITaskRepository repository) =>
_repository = repository;

public async void OnNavigatedTo(NavigationContext navigationContext) =>
await LoadTasksCommand.ExecuteAsync(null);

public void OnNavigatedFrom(NavigationContext navigationContext) { }

public bool IsNavigationTarget(NavigationContext navigationContext) => true;
}

При каждом входе на экран список подтягивается с сервера.


Обработка ошибок сети

[ObservableProperty]
private string? _errorMessage;

[RelayCommand]
private async Task LoadTasksAsync()
{
ErrorMessage = null;
IsBusy = true;
try
{
var items = await _repository.GetAllAsync();
Tasks.Clear();
foreach (var t in items)
Tasks.Add(TaskItemViewModel.FromModel(t));
}
catch (HttpRequestException ex)
{
ErrorMessage = "Сервер недоступен. Запустите TaskDesk.Api на порту 5100.";
// лог: ex.Message
}
finally
{
IsBusy = false;
}
}

В View — TextBlock с красным текстом, привязанный к ErrorMessage.


EventAggregator — опционально

Если позже появится второй View («Детали задачи»), можно оповестить список без жёсткой связи:

public class TaskChangedEvent : PubSubEvent { }

// после сохранения
_eventAggregator.GetEvent<TaskChangedEvent>().Publish();

// в TaskListViewModel — подписка в ctor, отписка в Destroy

Для минимального TaskDesk достаточно повторного LoadTasks после Create.


Запуск связки

Терминал 1:

dotnet run --project src/TaskDesk.Api --urls http://localhost:5100

Терминал 2:

dotnet run --project src/TaskDesk.Client

Чек-лист шага 4

  • PrismApplication создаёт ShellWindow и регистрирует навигацию.
  • TaskListViewModel получает ITaskRepository через конструктор.
  • ApiTaskRepository читает BaseUrl из конфигурации.
  • Ошибка сети отображается в UI, приложение не падает.

Дальше: Тестирование.


См. также


См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").