Практикум 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.cs | RegisterTypes, внедрение в VM |
| Навигация | Смена Content вручную | IRegionManager, URI TaskListView |
| Модульность | Один exe | IModule, 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, приложение не падает.
Дальше: Тестирование.
См. также
- Официальная документация Prism
- 1192.md — элементы WPF
- DI в .NET
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). WPF как презентационный слой .NET — дерево XAML, layout, привязки, ресурсы и связь с практикумом TaskDesk. Model, View, ViewModel, INotifyPropertyChanged, ICommand, CommunityToolkit.Mvvm и тестируемая логика для TaskDesk. REST API для TaskDesk — контроллеры, DTO, Swagger, CORS, in-memory хранилище и контракт для WPF-клиента. Postman и Swagger для REST TaskDesk, WebApplicationFactory, xUnit, Moq для ViewModel и репозитория. Полноценное клиент-серверное приложение — solution, сборка, сценарии демо, расширения и чек-лист готовности.Практикум WPF — введение в WPF и XAML
Практикум WPF — основы MVVM
Практикум WPF — сервер ASP.NET Core Web API
Практикум WPF — тестирование API и unit-тесты
Практикум WPF — итоговый проект TaskDesk