--- phase: 04 plan: 10 title: TemplatesViewModel + TemplatesView + DI Registration + MainWindow Wiring status: pending wave: 3 depends_on: - 04-02 - 04-06 - 04-07 - 04-08 - 04-09 files_modified: - SharepointToolbox/ViewModels/Tabs/TemplatesViewModel.cs - SharepointToolbox/Views/Tabs/TemplatesView.xaml - SharepointToolbox/Views/Tabs/TemplatesView.xaml.cs - SharepointToolbox/App.xaml.cs - SharepointToolbox/MainWindow.xaml - SharepointToolbox/MainWindow.xaml.cs autonomous: false requirements: - TMPL-01 - TMPL-02 - TMPL-03 - TMPL-04 must_haves: truths: - "TemplatesView shows a list of saved templates with capture, apply, rename, delete buttons" - "User can capture a template from a connected site with checkbox options" - "User can apply a template to create a new site" - "All Phase 4 services, ViewModels, and Views are registered in DI" - "All 5 new tabs appear in MainWindow (Transfer, Bulk Members, Bulk Sites, Folder Structure, Templates)" - "Application launches and all tabs are visible" artifacts: - path: "SharepointToolbox/ViewModels/Tabs/TemplatesViewModel.cs" provides: "Templates tab ViewModel" exports: ["TemplatesViewModel"] - path: "SharepointToolbox/Views/Tabs/TemplatesView.xaml" provides: "Templates tab UI" - path: "SharepointToolbox/App.xaml.cs" provides: "DI registration for all Phase 4 types" - path: "SharepointToolbox/MainWindow.xaml" provides: "5 new tab items replacing FeatureTabBase stubs" key_links: - from: "TemplatesViewModel.cs" to: "ITemplateService" via: "capture and apply operations" pattern: "CaptureTemplateAsync|ApplyTemplateAsync" - from: "TemplatesViewModel.cs" to: "TemplateRepository" via: "template CRUD" pattern: "TemplateRepository" - from: "App.xaml.cs" to: "All Phase 4 services" via: "DI registration" pattern: "AddTransient" - from: "MainWindow.xaml.cs" to: "All Phase 4 Views" via: "tab content wiring" pattern: "GetRequiredService" --- # Plan 04-10: TemplatesViewModel + TemplatesView + DI Registration + MainWindow Wiring ## Goal Create the Templates tab (ViewModel + View), register ALL Phase 4 services/ViewModels/Views in DI, wire all 5 new tabs in MainWindow, and verify the app launches with all tabs visible. ## Context All services are implemented: FileTransferService (04-03), BulkMemberService (04-04), BulkSiteService (04-05), TemplateService + FolderStructureService (04-06), CsvValidationService (04-02), TemplateRepository (04-02). All ViewModels/Views for Transfer (04-08), BulkMembers/BulkSites/FolderStructure (04-09) are done. DI pattern: Services as `AddTransient()`. ViewModels/Views as `AddTransient()`. Infrastructure singletons as `AddSingleton()`. Register in `App.xaml.cs RegisterServices()`. MainWindow pattern: Add `x:Name` TabItems in XAML, set Content from DI in code-behind constructor. Current MainWindow.xaml has 3 stub tabs (Templates, Bulk, Structure) with `FeatureTabBase`. These must be replaced with the 5 new named TabItems. ## Tasks ### Task 1: Create TemplatesViewModel + TemplatesView **Files:** - `SharepointToolbox/ViewModels/Tabs/TemplatesViewModel.cs` - `SharepointToolbox/Views/Tabs/TemplatesView.xaml` - `SharepointToolbox/Views/Tabs/TemplatesView.xaml.cs` **Action:** 1. Create `TemplatesViewModel.cs`: ```csharp using System.Collections.ObjectModel; using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.Logging; using Serilog; using SharepointToolbox.Core.Models; using SharepointToolbox.Infrastructure.Persistence; using SharepointToolbox.Localization; using SharepointToolbox.Services; namespace SharepointToolbox.ViewModels.Tabs; public partial class TemplatesViewModel : FeatureViewModelBase { private readonly ITemplateService _templateService; private readonly TemplateRepository _templateRepo; private readonly ISessionManager _sessionManager; private readonly ILogger _logger; private TenantProfile? _currentProfile; // Template list private ObservableCollection _templates = new(); public ObservableCollection Templates { get => _templates; private set { _templates = value; OnPropertyChanged(); } } [ObservableProperty] private SiteTemplate? _selectedTemplate; // Capture options [ObservableProperty] private string _captureSiteUrl = string.Empty; [ObservableProperty] private string _templateName = string.Empty; [ObservableProperty] private bool _captureLibraries = true; [ObservableProperty] private bool _captureFolders = true; [ObservableProperty] private bool _capturePermissions = true; [ObservableProperty] private bool _captureLogo = true; [ObservableProperty] private bool _captureSettings = true; // Apply options [ObservableProperty] private string _newSiteTitle = string.Empty; [ObservableProperty] private string _newSiteAlias = string.Empty; public IAsyncRelayCommand CaptureCommand { get; } public IAsyncRelayCommand ApplyCommand { get; } public IAsyncRelayCommand RenameCommand { get; } public IAsyncRelayCommand DeleteCommand { get; } public IAsyncRelayCommand RefreshCommand { get; } public TenantProfile? CurrentProfile => _currentProfile; public TemplatesViewModel( ITemplateService templateService, TemplateRepository templateRepo, ISessionManager sessionManager, ILogger logger) : base(logger) { _templateService = templateService; _templateRepo = templateRepo; _sessionManager = sessionManager; _logger = logger; CaptureCommand = new AsyncRelayCommand(CaptureAsync, () => !IsRunning); ApplyCommand = new AsyncRelayCommand(ApplyAsync, () => !IsRunning && SelectedTemplate != null); RenameCommand = new AsyncRelayCommand(RenameAsync, () => SelectedTemplate != null); DeleteCommand = new AsyncRelayCommand(DeleteAsync, () => SelectedTemplate != null); RefreshCommand = new AsyncRelayCommand(RefreshListAsync); } protected override async Task RunOperationAsync(CancellationToken ct, IProgress progress) { // Not used directly — Capture and Apply have their own async commands await Task.CompletedTask; } private async Task CaptureAsync() { if (_currentProfile == null) throw new InvalidOperationException("No tenant connected."); if (string.IsNullOrWhiteSpace(CaptureSiteUrl)) throw new InvalidOperationException("Site URL is required."); if (string.IsNullOrWhiteSpace(TemplateName)) throw new InvalidOperationException("Template name is required."); try { IsRunning = true; StatusMessage = "Capturing template..."; var profile = new TenantProfile { Name = _currentProfile.Name, TenantUrl = CaptureSiteUrl, ClientId = _currentProfile.ClientId, }; var ctx = await _sessionManager.GetOrCreateContextAsync(profile, CancellationToken.None); var options = new SiteTemplateOptions { CaptureLibraries = CaptureLibraries, CaptureFolders = CaptureFolders, CapturePermissionGroups = CapturePermissions, CaptureLogo = CaptureLogo, CaptureSettings = CaptureSettings, }; var progress = new Progress(p => StatusMessage = p.Message); var template = await _templateService.CaptureTemplateAsync(ctx, options, progress, CancellationToken.None); template.Name = TemplateName; await _templateRepo.SaveAsync(template); Log.Information("Template captured: {Name} from {Url}", template.Name, CaptureSiteUrl); await RefreshListAsync(); StatusMessage = $"Template '{TemplateName}' captured successfully."; } catch (Exception ex) { StatusMessage = $"Capture failed: {ex.Message}"; Log.Error(ex, "Template capture failed"); } finally { IsRunning = false; } } private async Task ApplyAsync() { if (_currentProfile == null || SelectedTemplate == null) return; if (string.IsNullOrWhiteSpace(NewSiteTitle)) throw new InvalidOperationException("New site title is required."); if (string.IsNullOrWhiteSpace(NewSiteAlias)) throw new InvalidOperationException("New site alias is required."); try { IsRunning = true; StatusMessage = $"Applying template '{SelectedTemplate.Name}'..."; var ctx = await _sessionManager.GetOrCreateContextAsync(_currentProfile, CancellationToken.None); var progress = new Progress(p => StatusMessage = p.Message); var siteUrl = await _templateService.ApplyTemplateAsync( ctx, SelectedTemplate, NewSiteTitle, NewSiteAlias, progress, CancellationToken.None); StatusMessage = $"Template applied. Site created at: {siteUrl}"; Log.Information("Template '{Name}' applied. New site: {Url}", SelectedTemplate.Name, siteUrl); } catch (Exception ex) { StatusMessage = $"Apply failed: {ex.Message}"; Log.Error(ex, "Template apply failed"); } finally { IsRunning = false; } } private async Task RenameAsync() { if (SelectedTemplate == null) return; // Simple input dialog — use a prompt via code-behind or InputBox // The View will wire this via a Func factory if (RenameDialogFactory != null) { var newName = RenameDialogFactory(SelectedTemplate.Name); if (!string.IsNullOrWhiteSpace(newName)) { await _templateRepo.RenameAsync(SelectedTemplate.Id, newName); await RefreshListAsync(); Log.Information("Template renamed: {OldName} -> {NewName}", SelectedTemplate.Name, newName); } } } private async Task DeleteAsync() { if (SelectedTemplate == null) return; await _templateRepo.DeleteAsync(SelectedTemplate.Id); await RefreshListAsync(); Log.Information("Template deleted: {Name}", SelectedTemplate.Name); } private async Task RefreshListAsync() { var templates = await _templateRepo.GetAllAsync(); await Application.Current.Dispatcher.InvokeAsync(() => { Templates = new ObservableCollection(templates); }); } // Factory for rename dialog — set by View code-behind public Func? RenameDialogFactory { get; set; } protected override void OnTenantSwitched(TenantProfile profile) { _currentProfile = profile; CaptureSiteUrl = string.Empty; TemplateName = string.Empty; NewSiteTitle = string.Empty; NewSiteAlias = string.Empty; StatusMessage = string.Empty; // Refresh template list on tenant switch _ = RefreshListAsync(); } partial void OnSelectedTemplateChanged(SiteTemplate? value) { ApplyCommand.NotifyCanExecuteChanged(); RenameCommand.NotifyCanExecuteChanged(); DeleteCommand.NotifyCanExecuteChanged(); } } ``` 2. Create `TemplatesView.xaml`: ```xml