chore: complete v1.0 milestone
Archive 5 phases (36 plans) to milestones/v1.0-phases/. Archive roadmap, requirements, and audit to milestones/. Evolve PROJECT.md with shipped state and validated requirements. Collapse ROADMAP.md to one-line milestone summary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,575 @@
|
||||
---
|
||||
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`
|
||||
Reference in New Issue
Block a user