All checks were successful
Release zip package / release (push) Successful in 10s
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>
576 lines
24 KiB
Markdown
576 lines
24 KiB
Markdown
---
|
|
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`
|