---
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<Interface, Implementation>()`. ViewModels/Views as `AddTransient<Type>()`. Infrastructure singletons as `AddSingleton<Type>()`. 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<FeatureViewModelBase> _logger;
    private TenantProfile? _currentProfile;

    // Template list
    private ObservableCollection<SiteTemplate> _templates = new();
    public ObservableCollection<SiteTemplate> 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<FeatureViewModelBase> 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<OperationProgress> 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<OperationProgress>(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<OperationProgress>(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<string, string?> 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<SiteTemplate>(templates);
        });
    }

    // Factory for rename dialog — set by View code-behind
    public Func<string, string?>? 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
<UserControl x:Class="SharepointToolbox.Views.Tabs.TemplatesView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:loc="clr-namespace:SharepointToolbox.Localization">
    <DockPanel Margin="10">
        <!-- Left panel: Capture and Apply -->
        <StackPanel DockPanel.Dock="Left" Width="320" Margin="0,0,10,0">
            <!-- Capture Section -->
            <GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.capture]}"
                      Margin="0,0,0,10">
                <StackPanel Margin="5">
                    <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.siteurl]}"
                               Margin="0,0,0,3" />
                    <TextBox Text="{Binding CaptureSiteUrl, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,5" />

                    <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.name]}"
                               Margin="0,0,0,3" />
                    <TextBox Text="{Binding TemplateName, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10" />

                    <!-- Capture options checkboxes -->
                    <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.options]}"
                               FontWeight="SemiBold" Margin="0,0,0,5" />
                    <CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.opt.libraries]}"
                              IsChecked="{Binding CaptureLibraries}" Margin="0,0,0,3" />
                    <CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.opt.folders]}"
                              IsChecked="{Binding CaptureFolders}" Margin="0,0,0,3" />
                    <CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.opt.permissions]}"
                              IsChecked="{Binding CapturePermissions}" Margin="0,0,0,3" />
                    <CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.opt.logo]}"
                              IsChecked="{Binding CaptureLogo}" Margin="0,0,0,3" />
                    <CheckBox Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.opt.settings]}"
                              IsChecked="{Binding CaptureSettings}" Margin="0,0,0,10" />

                    <Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.capture]}"
                            Command="{Binding CaptureCommand}" />
                </StackPanel>
            </GroupBox>

            <!-- Apply Section -->
            <GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.apply]}"
                      Margin="0,0,0,10">
                <StackPanel Margin="5">
                    <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.newtitle]}"
                               Margin="0,0,0,3" />
                    <TextBox Text="{Binding NewSiteTitle, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,5" />

                    <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.newalias]}"
                               Margin="0,0,0,3" />
                    <TextBox Text="{Binding NewSiteAlias, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10" />

                    <Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.apply]}"
                            Command="{Binding ApplyCommand}" />
                </StackPanel>
            </GroupBox>

            <!-- Progress -->
            <TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap" Margin="0,5,0,0" />
        </StackPanel>

        <!-- Right panel: Template list -->
        <DockPanel>
            <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,5">
                <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.list]}"
                           FontWeight="Bold" FontSize="14" VerticalAlignment="Center" Margin="0,0,10,0" />
                <Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.rename]}"
                        Command="{Binding RenameCommand}" Margin="0,0,5,0" Padding="10,3" />
                <Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[templates.delete]}"
                        Command="{Binding DeleteCommand}" Padding="10,3" />
            </StackPanel>

            <DataGrid ItemsSource="{Binding Templates}" SelectedItem="{Binding SelectedTemplate}"
                      AutoGenerateColumns="False" IsReadOnly="True"
                      SelectionMode="Single" CanUserSortColumns="True">
                <DataGrid.Columns>
                    <DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*" />
                    <DataGridTextColumn Header="Type" Binding="{Binding SiteType}" Width="100" />
                    <DataGridTextColumn Header="Source" Binding="{Binding SourceUrl}" Width="*" />
                    <DataGridTextColumn Header="Captured" Binding="{Binding CapturedAt, StringFormat=yyyy-MM-dd HH:mm}" Width="140" />
                </DataGrid.Columns>
            </DataGrid>
        </DockPanel>
    </DockPanel>
</UserControl>
```

3. Create `TemplatesView.xaml.cs`:
```csharp
using System.Windows;
using System.Windows.Controls;
using Microsoft.VisualBasic;

namespace SharepointToolbox.Views.Tabs;

public partial class TemplatesView : UserControl
{
    public TemplatesView(ViewModels.Tabs.TemplatesViewModel viewModel)
    {
        InitializeComponent();
        DataContext = viewModel;

        // Wire rename dialog factory — use simple InputBox
        viewModel.RenameDialogFactory = currentName =>
        {
            // Simple prompt — WPF has no built-in InputBox, use Microsoft.VisualBasic.Interaction.InputBox
            // or create a simple dialog. For simplicity, use a MessageBox approach.
            var result = Microsoft.VisualBasic.Interaction.InputBox(
                "Enter new template name:", "Rename Template", currentName);
            return string.IsNullOrWhiteSpace(result) ? null : result;
        };

        // Load templates on first display
        viewModel.RefreshCommand.ExecuteAsync(null);
    }
}
```

Note: If `Microsoft.VisualBasic` is not available or undesired, create a simple `InputDialog` Window instead. The executor should check if `Microsoft.VisualBasic` is referenced (it's part of .NET SDK by default) or create a minimal WPF dialog.

**Verify:**
```bash
dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore -q
```

**Done:** TemplatesViewModel and TemplatesView compile. Template list, capture with checkboxes, apply with title/alias, rename, delete all connected.

### Task 2: Register all Phase 4 types in DI + Wire MainWindow tabs

**Files:**
- `SharepointToolbox/App.xaml.cs`
- `SharepointToolbox/MainWindow.xaml`
- `SharepointToolbox/MainWindow.xaml.cs`

**Action:**

1. Update `App.xaml.cs` — add Phase 4 DI registrations in `RegisterServices()`, after the existing Phase 3 block:

```csharp
// Add these using statements at the top:
using SharepointToolbox.Infrastructure.Auth;
// (other usings already present)

// Add in RegisterServices(), after Phase 3 block:

// Phase 4: Bulk Operations Infrastructure
var templatesDir = Path.Combine(appData, "templates");
services.AddSingleton(_ => new TemplateRepository(templatesDir));
services.AddSingleton<GraphClientFactory>();
services.AddTransient<ICsvValidationService, CsvValidationService>();
services.AddTransient<BulkResultCsvExportService>();

// Phase 4: File Transfer
services.AddTransient<IFileTransferService, FileTransferService>();
services.AddTransient<TransferViewModel>();
services.AddTransient<TransferView>();

// Phase 4: Bulk Members
services.AddTransient<IBulkMemberService, BulkMemberService>();
services.AddTransient<BulkMembersViewModel>();
services.AddTransient<BulkMembersView>();

// Phase 4: Bulk Sites
services.AddTransient<IBulkSiteService, BulkSiteService>();
services.AddTransient<BulkSitesViewModel>();
services.AddTransient<BulkSitesView>();

// Phase 4: Templates
services.AddTransient<ITemplateService, TemplateService>();
services.AddTransient<TemplatesViewModel>();
services.AddTransient<TemplatesView>();

// Phase 4: Folder Structure
services.AddTransient<IFolderStructureService, FolderStructureService>();
services.AddTransient<FolderStructureViewModel>();
services.AddTransient<FolderStructureView>();
```

Also add required using statements at top of App.xaml.cs:
```csharp
using SharepointToolbox.Infrastructure.Auth;  // GraphClientFactory
// Other new usings should be covered by existing namespace imports
```

2. Update `MainWindow.xaml` — replace the 3 FeatureTabBase stub tabs (Templates, Bulk, Structure) with 5 named TabItems:

Replace:
```xml
<TabItem Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.templates]}">
    <controls:FeatureTabBase />
</TabItem>
<TabItem Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.bulk]}">
    <controls:FeatureTabBase />
</TabItem>
<TabItem Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.structure]}">
    <controls:FeatureTabBase />
</TabItem>
```

With:
```xml
<TabItem x:Name="TransferTabItem"
         Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.transfer]}">
</TabItem>
<TabItem x:Name="BulkMembersTabItem"
         Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.bulkMembers]}">
</TabItem>
<TabItem x:Name="BulkSitesTabItem"
         Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.bulkSites]}">
</TabItem>
<TabItem x:Name="FolderStructureTabItem"
         Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.folderStructure]}">
</TabItem>
<TabItem x:Name="TemplatesTabItem"
         Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[tab.templates]}">
</TabItem>
```

Note: Keep the Settings tab at the end. The tab order should be: Permissions, Storage, Search, Duplicates, Transfer, Bulk Members, Bulk Sites, Folder Structure, Templates, Settings.

3. Update `MainWindow.xaml.cs` — add tab content wiring in the constructor, after existing tab assignments:

```csharp
// Add after existing DuplicatesTabItem.Content line:

// Phase 4: Replace stub tabs with DI-resolved Views
TransferTabItem.Content = serviceProvider.GetRequiredService<TransferView>();
BulkMembersTabItem.Content = serviceProvider.GetRequiredService<BulkMembersView>();
BulkSitesTabItem.Content = serviceProvider.GetRequiredService<BulkSitesView>();
FolderStructureTabItem.Content = serviceProvider.GetRequiredService<FolderStructureView>();
TemplatesTabItem.Content = serviceProvider.GetRequiredService<TemplatesView>();
```

**Verify:**
```bash
dotnet build SharepointToolbox.slnx --no-restore -q && dotnet test SharepointToolbox.Tests --no-build -q
```

**Done:** All Phase 4 services, ViewModels, and Views registered in DI. All 5 new tabs wired in MainWindow. Application builds and all tests pass.

### Task 3: Visual checkpoint

**Type:** checkpoint:human-verify

**What-built:** All 5 Phase 4 tabs (Transfer, Bulk Members, Bulk Sites, Folder Structure, Templates) integrated into the application.

**How-to-verify:**
1. Run the application: `dotnet run --project SharepointToolbox/SharepointToolbox.csproj`
2. Verify all 10 tabs are visible: Permissions, Storage, Search, Duplicates, Transfer, Bulk Members, Bulk Sites, Folder Structure, Templates, Settings
3. Click each new tab — verify it shows the expected layout (no crash, no blank tab)
4. On Bulk Members tab: click "Load Example" — verify the DataGrid populates with sample member data
5. On Bulk Sites tab: click "Load Example" — verify the DataGrid populates with sample site data
6. On Folder Structure tab: click "Load Example" — verify the DataGrid populates with folder structure data
7. On Templates tab: verify the capture options section shows 5 checkboxes (Libraries, Folders, Permission Groups, Logo, Settings)
8. On Transfer tab: verify source/destination sections with Browse buttons are visible

**Resume-signal:** Type "approved" or describe issues.

**Commit:** `feat(04-10): register Phase 4 DI + wire MainWindow tabs + TemplatesView`
