Research covers stack (NET10/WPF/PnP.Framework), features (v1 parity + v1.x differentiators), architecture (MVVM four-layer pattern), and pitfalls (10 critical pitfalls all addressed in foundation phase). SUMMARY.md synthesizes findings with phase-structured roadmap implications. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
582 lines
29 KiB
Markdown
582 lines
29 KiB
Markdown
# Architecture Research
|
|
|
|
**Domain:** C#/WPF SharePoint Online Administration Desktop Tool
|
|
**Researched:** 2026-04-02
|
|
**Confidence:** HIGH
|
|
|
|
## Standard Architecture
|
|
|
|
### System Overview
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────┐
|
|
│ PRESENTATION LAYER │
|
|
│ ┌──────────────┐ ┌─────────────────────────────────────────────┐ │
|
|
│ │ MainWindow │ │ Feature Views (XAML) │ │
|
|
│ │ Shell.xaml │ │ Permissions │ Storage │ Search │ Templates │ │
|
|
│ │ │ │ Duplicates │ Bulk │ Reports │ Settings │ │
|
|
│ └──────┬───────┘ └──────────────────────┬────────────────────┘ │
|
|
│ │ DataContext binding │ DataContext binding │
|
|
├─────────┴─────────────────────────────────┴────────────────────────┤
|
|
│ VIEWMODEL LAYER │
|
|
│ ┌─────────────┐ ┌──────────────────────────────────────────────┐ │
|
|
│ │ MainWindow │ │ Feature ViewModels │ │
|
|
│ │ ViewModel │ │ PermissionsVM │ StorageVM │ SearchVM │ │
|
|
│ │ (nav/shell)│ │ TemplatesVM │ BulkOpsVM │ DuplicatesVM │ │
|
|
│ └──────┬──────┘ └───────────────────────┬──────────────────────┘ │
|
|
│ │ ICommand, ObservableProperty │ AsyncRelayCommand │
|
|
├─────────┴─────────────────────────────────┴────────────────────────┤
|
|
│ SERVICE LAYER │
|
|
│ ┌────────────────┐ ┌─────────────────┐ ┌──────────────────────┐ │
|
|
│ │ AuthService │ │ SharePoint │ │ Cross-Cutting │ │
|
|
│ │ SessionManager │ │ Feature Services │ │ Services │ │
|
|
│ │ TenantSession │ │ PermissionsService│ │ ReportExportService │ │
|
|
│ │ │ │ StorageService │ │ LocalizationService │ │
|
|
│ │ │ │ SearchService │ │ DialogService │ │
|
|
│ │ │ │ TemplateService │ │ SettingsService │ │
|
|
│ └────────┬───────┘ └────────┬────────┘ └──────────────────────┘ │
|
|
│ │ ClientContext │ IProgress<T>, CancellationToken │
|
|
├───────────┴────────────────────┴────────────────────────────────────┤
|
|
│ INFRASTRUCTURE / INTEGRATION LAYER │
|
|
│ ┌──────────────────┐ ┌───────────────────┐ ┌──────────────────┐ │
|
|
│ │ PnP Framework │ │ Microsoft Graph │ │ Local Storage │ │
|
|
│ │ AuthManager │ │ GraphServiceClient │ │ JSON Files │ │
|
|
│ │ ClientContext │ │ (Graph operations) │ │ Profiles │ │
|
|
│ │ (CSOM ops) │ │ │ │ Templates │ │
|
|
│ └──────────────────┘ └───────────────────┘ └──────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Component Responsibilities
|
|
|
|
| Component | Responsibility | Typical Implementation |
|
|
|-----------|----------------|------------------------|
|
|
| MainWindow Shell | Tab navigation, tenant selector, app chrome, log panel | XAML with TabControl or navigation frame |
|
|
| Feature Views | User input forms, result grids, progress indicators | UserControl XAML, zero code-behind |
|
|
| Feature ViewModels | Commands, observable state, orchestrates services | ObservableObject subclass, AsyncRelayCommand |
|
|
| AuthService / SessionManager | Multi-tenant session lifecycle, token cache, active tenant state | Singleton, MSAL token cache per tenant |
|
|
| TenantSession | Per-tenant PnP ClientContext + auth token | Immutable record, created by AuthService |
|
|
| SharePoint Feature Services | Domain logic that calls PnP Framework or Graph | Stateless class, injectable, cancellable |
|
|
| ReportExportService | HTML/CSV generation from result models | Stateless, template-based string builder |
|
|
| LocalizationService | Key-based EN/FR translation, dynamic language switch | Singleton, loads lang/*.json, INotifyPropertyChanged |
|
|
| SettingsService | Read/write JSON settings, profiles, templates | Singleton, file I/O wrapped in async |
|
|
| DialogService | Open files, show message boxes, pick folders | Interface + WPF implementation, testable |
|
|
|
|
---
|
|
|
|
## Recommended Project Structure
|
|
|
|
```
|
|
SharepointToolbox/
|
|
├── App.xaml # Application entry, DI container bootstrap
|
|
├── App.xaml.cs # Host builder, service registration
|
|
│
|
|
├── Core/ # Domain models — no WPF dependencies
|
|
│ ├── Models/
|
|
│ │ ├── PermissionEntry.cs
|
|
│ │ ├── StorageMetrics.cs
|
|
│ │ ├── SiteTemplate.cs
|
|
│ │ ├── TenantProfile.cs
|
|
│ │ └── SearchResult.cs
|
|
│ ├── Interfaces/
|
|
│ │ ├── IAuthService.cs
|
|
│ │ ├── IPermissionsService.cs
|
|
│ │ ├── IStorageService.cs
|
|
│ │ ├── ISearchService.cs
|
|
│ │ ├── ITemplateService.cs
|
|
│ │ ├── IBulkOpsService.cs
|
|
│ │ ├── IDuplicateService.cs
|
|
│ │ ├── IReportExportService.cs
|
|
│ │ ├── ISettingsService.cs
|
|
│ │ ├── ILocalizationService.cs
|
|
│ │ └── IDialogService.cs
|
|
│ └── Exceptions/
|
|
│ ├── SharePointConnectionException.cs
|
|
│ └── AuthenticationException.cs
|
|
│
|
|
├── Services/ # Business logic + infrastructure
|
|
│ ├── Auth/
|
|
│ │ ├── AuthService.cs # PnP AuthenticationManager wrapper
|
|
│ │ ├── SessionManager.cs # Multi-tenant session store
|
|
│ │ └── TenantSession.cs # Per-tenant PnP ClientContext holder
|
|
│ ├── SharePoint/
|
|
│ │ ├── PermissionsService.cs # Recursive permission scanning
|
|
│ │ ├── StorageService.cs # Storage metric traversal
|
|
│ │ ├── SearchService.cs # KQL-based search via PnP/Graph
|
|
│ │ ├── TemplateService.cs # Capture & apply site templates
|
|
│ │ ├── DuplicateService.cs # File/folder duplicate detection
|
|
│ │ └── BulkOpsService.cs # Transfer, site creation, member add
|
|
│ ├── Reporting/
|
|
│ │ ├── HtmlReportService.cs # Self-contained HTML + JS reports
|
|
│ │ └── CsvExportService.cs # CSV export
|
|
│ ├── LocalizationService.cs # EN/FR key-value translations
|
|
│ ├── SettingsService.cs # JSON profiles, templates, settings
|
|
│ └── DialogService.cs # WPF dialog abstractions
|
|
│
|
|
├── ViewModels/ # WPF-aware but UI-framework-agnostic
|
|
│ ├── MainWindowViewModel.cs # Shell nav, tenant switcher, log
|
|
│ ├── Permissions/
|
|
│ │ └── PermissionsViewModel.cs
|
|
│ ├── Storage/
|
|
│ │ └── StorageViewModel.cs
|
|
│ ├── Search/
|
|
│ │ └── SearchViewModel.cs
|
|
│ ├── Templates/
|
|
│ │ └── TemplatesViewModel.cs
|
|
│ ├── Duplicates/
|
|
│ │ └── DuplicatesViewModel.cs
|
|
│ ├── BulkOps/
|
|
│ │ └── BulkOpsViewModel.cs
|
|
│ └── Settings/
|
|
│ └── SettingsViewModel.cs
|
|
│
|
|
├── Views/ # XAML — no business logic
|
|
│ ├── MainWindow.xaml
|
|
│ ├── Permissions/
|
|
│ │ └── PermissionsView.xaml
|
|
│ ├── Storage/
|
|
│ │ └── StorageView.xaml
|
|
│ ├── Search/
|
|
│ │ └── SearchView.xaml
|
|
│ ├── Templates/
|
|
│ │ └── TemplatesView.xaml
|
|
│ ├── Duplicates/
|
|
│ │ └── DuplicatesView.xaml
|
|
│ ├── BulkOps/
|
|
│ │ └── BulkOpsView.xaml
|
|
│ └── Settings/
|
|
│ └── SettingsView.xaml
|
|
│
|
|
├── Controls/ # Reusable WPF controls
|
|
│ ├── TenantSelectorControl.xaml
|
|
│ ├── LogPanelControl.xaml
|
|
│ ├── ProgressOverlayControl.xaml
|
|
│ └── StorageChartControl.xaml # LiveCharts2 wrapper
|
|
│
|
|
├── Converters/ # IValueConverter implementations
|
|
│ ├── BytesToStringConverter.cs
|
|
│ ├── BoolToVisibilityConverter.cs
|
|
│ └── PermissionColorConverter.cs
|
|
│
|
|
├── Resources/ # Styles, brushes, theme
|
|
│ ├── Styles.xaml
|
|
│ └── Colors.xaml
|
|
│
|
|
├── Lang/ # Language files
|
|
│ ├── en.json
|
|
│ └── fr.json
|
|
│
|
|
└── Infrastructure/
|
|
└── Behaviors/ # XAML attached behaviors (no code-behind workaround)
|
|
└── ScrollToBottomBehavior.cs
|
|
```
|
|
|
|
### Structure Rationale
|
|
|
|
- **Core/**: Pure C# — no WPF references. Interfaces here make services testable. Models are plain data classes.
|
|
- **Services/**: All domain logic and I/O. Injected via constructor DI. No static state.
|
|
- **ViewModels/**: Mirror the feature structure. Depend on service interfaces, never on concrete implementations.
|
|
- **Views/**: XAML-only. No logic. `DataContext` set by DI or ViewModelLocator pattern at startup.
|
|
- **Controls/**: Reusable UI widgets that encapsulate chart, log, and progress concerns.
|
|
|
|
---
|
|
|
|
## Architectural Patterns
|
|
|
|
### Pattern 1: ObservableObject + AsyncRelayCommand (CommunityToolkit.Mvvm)
|
|
|
|
**What:** Use `ObservableObject` as base class for all ViewModels. Use `[ObservableProperty]` source-gen attribute for bindable properties. Use `AsyncRelayCommand` (with `CancellationToken`) for all SharePoint operations.
|
|
|
|
**When to use:** All ViewModels. This is the standard pattern for .NET 8 + WPF.
|
|
|
|
**Trade-offs:** Source generators require C# 10+. Generated partial class syntax is unfamiliar at first but eliminates 80% of boilerplate.
|
|
|
|
**Example:**
|
|
```csharp
|
|
public partial class PermissionsViewModel : ObservableObject
|
|
{
|
|
private readonly IPermissionsService _permissionsService;
|
|
|
|
[ObservableProperty]
|
|
private bool _isRunning;
|
|
|
|
[ObservableProperty]
|
|
private string _statusMessage = string.Empty;
|
|
|
|
[ObservableProperty]
|
|
private ObservableCollection<PermissionEntry> _results = new();
|
|
|
|
public IAsyncRelayCommand RunReportCommand { get; }
|
|
|
|
public PermissionsViewModel(IPermissionsService permissionsService)
|
|
{
|
|
_permissionsService = permissionsService;
|
|
RunReportCommand = new AsyncRelayCommand(RunReportAsync, allowConcurrentExecutions: false);
|
|
}
|
|
|
|
private async Task RunReportAsync(CancellationToken cancellationToken)
|
|
{
|
|
IsRunning = true;
|
|
StatusMessage = "Scanning permissions...";
|
|
try
|
|
{
|
|
var results = await _permissionsService.ScanAsync(
|
|
SiteUrl, cancellationToken,
|
|
new Progress<string>(msg => StatusMessage = msg));
|
|
Results = new ObservableCollection<PermissionEntry>(results);
|
|
}
|
|
finally { IsRunning = false; }
|
|
}
|
|
}
|
|
```
|
|
|
|
### Pattern 2: Multi-Tenant Session Manager
|
|
|
|
**What:** A singleton `SessionManager` holds a dictionary of `TenantSession` objects keyed by tenant URL. When the user selects a tenant profile, the session is reused if still valid (MSAL token cache handles token refresh). No re-authentication unless the token is expired and silent refresh fails.
|
|
|
|
**When to use:** Every SharePoint service operation resolves `IAuthService.GetSessionAsync(tenantUrl)` before calling PnP Framework.
|
|
|
|
**Trade-offs:** MSAL token cache must be persisted across app restarts for seamless reconnect. For interactive login, MSAL `PublicClientApplicationBuilder` with `WithParentActivityOrWindow` is required on Windows to avoid a blank browser window.
|
|
|
|
**Example:**
|
|
```csharp
|
|
public class SessionManager
|
|
{
|
|
private readonly ConcurrentDictionary<string, TenantSession> _sessions = new();
|
|
|
|
public async Task<TenantSession> GetOrCreateSessionAsync(
|
|
TenantProfile profile, CancellationToken ct)
|
|
{
|
|
if (_sessions.TryGetValue(profile.TenantUrl, out var session)
|
|
&& !session.IsExpired)
|
|
return session;
|
|
|
|
var authManager = new PnP.Framework.AuthenticationManager(
|
|
profile.ClientId,
|
|
openBrowserCallback: url => Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }));
|
|
|
|
var ctx = await authManager.GetContextAsync(profile.TenantUrl);
|
|
var newSession = new TenantSession(profile, ctx, authManager);
|
|
_sessions[profile.TenantUrl] = newSession;
|
|
return newSession;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Pattern 3: IProgress\<T\> + CancellationToken for All Long Operations
|
|
|
|
**What:** Every service method that calls SharePoint accepts `IProgress<OperationProgress>` and `CancellationToken`. The ViewModel creates `Progress<T>` (which marshals callbacks to the UI thread automatically) and `CancellationTokenSource`.
|
|
|
|
**When to use:** All SharePoint service methods. This replaces the PowerShell runspace + timer polling pattern from the existing app.
|
|
|
|
**Trade-offs:** `Progress<T>` captures SynchronizationContext on creation — must be created on the UI thread (i.e., inside the ViewModel, not inside the service).
|
|
|
|
**Example:**
|
|
```csharp
|
|
// In ViewModel (UI thread context):
|
|
var cts = new CancellationTokenSource();
|
|
CancelCommand = new RelayCommand(() => cts.Cancel());
|
|
var progress = new Progress<OperationProgress>(p => StatusMessage = p.Message);
|
|
|
|
// In Service (any thread):
|
|
public async Task<IList<PermissionEntry>> ScanAsync(
|
|
string siteUrl,
|
|
CancellationToken ct,
|
|
IProgress<OperationProgress> progress)
|
|
{
|
|
progress.Report(new OperationProgress("Connecting..."));
|
|
using var ctx = await _sessionManager.GetOrCreateSessionAsync(..., ct);
|
|
// ... recursive scanning ...
|
|
ct.ThrowIfCancellationRequested();
|
|
progress.Report(new OperationProgress($"Found {results.Count} entries"));
|
|
return results;
|
|
}
|
|
```
|
|
|
|
### Pattern 4: Messenger for Cross-ViewModel Events
|
|
|
|
**What:** Use `CommunityToolkit.Mvvm.Messaging.WeakReferenceMessenger` for decoupled communication between ViewModels (e.g., "tenant switched" notifies all feature VMs to reset state, "log entry added" updates the log panel ViewModel).
|
|
|
|
**When to use:** When two ViewModels need to communicate without direct reference (shell ↔ feature VMs, service callbacks ↔ log panel).
|
|
|
|
**Trade-offs:** Weak references mean recipients must be alive (held by DI container). Don't use for per-request data passing — use method return values for that.
|
|
|
|
### Pattern 5: Dependency Injection via Microsoft.Extensions.Hosting
|
|
|
|
**What:** Bootstrap the app with `Host.CreateDefaultBuilder()` in `App.xaml.cs`. Register all services, ViewModels, and the main window in the DI container. Use constructor injection everywhere — no service locator anti-pattern.
|
|
|
|
**Example:**
|
|
```csharp
|
|
// App.xaml.cs
|
|
protected override void OnStartup(StartupEventArgs e)
|
|
{
|
|
_host = Host.CreateDefaultBuilder()
|
|
.ConfigureServices(services =>
|
|
{
|
|
// Core services (singletons)
|
|
services.AddSingleton<ISettingsService, SettingsService>();
|
|
services.AddSingleton<ILocalizationService, LocalizationService>();
|
|
services.AddSingleton<SessionManager>();
|
|
services.AddSingleton<IAuthService, AuthService>();
|
|
services.AddSingleton<IDialogService, DialogService>();
|
|
|
|
// Feature services (transient — no shared state)
|
|
services.AddTransient<IPermissionsService, PermissionsService>();
|
|
services.AddTransient<IStorageService, StorageService>();
|
|
services.AddTransient<ISearchService, SearchService>();
|
|
|
|
// ViewModels
|
|
services.AddTransient<MainWindowViewModel>();
|
|
services.AddTransient<PermissionsViewModel>();
|
|
services.AddTransient<StorageViewModel>();
|
|
|
|
// Views
|
|
services.AddSingleton<MainWindow>();
|
|
})
|
|
.Build();
|
|
|
|
_host.Start();
|
|
var mainWindow = _host.Services.GetRequiredService<MainWindow>();
|
|
mainWindow.DataContext = _host.Services.GetRequiredService<MainWindowViewModel>();
|
|
mainWindow.Show();
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Data Flow
|
|
|
|
### SharePoint Operation Request Flow
|
|
|
|
```
|
|
User clicks "Run" button
|
|
↓
|
|
View command binding triggers AsyncRelayCommand.ExecuteAsync()
|
|
↓
|
|
ViewModel validates inputs → creates CancellationTokenSource + Progress<T>
|
|
↓
|
|
ViewModel calls IFeatureService.ScanAsync(params, ct, progress)
|
|
↓
|
|
Service calls SessionManager.GetOrCreateSessionAsync(profile, ct)
|
|
↓
|
|
SessionManager checks cache → reuses token or triggers interactive login
|
|
↓
|
|
Service executes PnP Framework / Graph SDK calls (async, awaited)
|
|
↓
|
|
Service reports incremental progress → Progress<T>.Report() → UI thread
|
|
↓
|
|
Service returns result collection to ViewModel
|
|
↓
|
|
ViewModel updates ObservableCollection → WPF binding refreshes DataGrid
|
|
↓
|
|
ViewModel sets IsRunning = false → progress overlay hides
|
|
```
|
|
|
|
### Authentication & Session Flow
|
|
|
|
```
|
|
User selects tenant profile from dropdown
|
|
↓
|
|
MainWindowViewModel calls SessionManager.SetActiveProfile(profile)
|
|
↓
|
|
SessionManager publishes TenantChangedMessage via WeakReferenceMessenger
|
|
↓
|
|
All feature ViewModels receive message → reset their state/results
|
|
↓
|
|
On first operation: SessionManager.GetOrCreateSessionAsync()
|
|
↓
|
|
[Cache hit: token valid] → return existing ClientContext immediately
|
|
[Cache miss / expired] → PnP AuthManager.GetContextAsync()
|
|
↓
|
|
MSAL silent token refresh attempt
|
|
↓
|
|
[Silent fails] → open browser for interactive login
|
|
↓
|
|
User authenticates → token cached by MSAL
|
|
↓
|
|
ClientContext returned to caller
|
|
```
|
|
|
|
### Report Export Flow
|
|
|
|
```
|
|
Service returns List<TModel> to ViewModel
|
|
↓
|
|
User clicks "Export CSV" or "Export HTML"
|
|
↓
|
|
ViewModel calls IReportExportService.ExportAsync(results, format, outputPath)
|
|
↓
|
|
ReportExportService generates file (string building, no blocking I/O on UI thread)
|
|
↓
|
|
ViewModel calls IDialogService.OpenFile(outputPath) to auto-open result
|
|
```
|
|
|
|
### State Management
|
|
|
|
```
|
|
AppState (DI-managed singletons):
|
|
SessionManager → active profile, tenant sessions dict
|
|
SettingsService → user prefs, data folder, profiles list
|
|
LocalizationService → current language, translation dict
|
|
|
|
Per-Operation State (ViewModel-local):
|
|
ObservableCollection<T> → bound to DataGrid
|
|
CancellationTokenSource → cancel button binding
|
|
IsRunning (bool) → progress overlay binding
|
|
StatusMessage (string) → progress label binding
|
|
```
|
|
|
|
---
|
|
|
|
## Component Boundaries
|
|
|
|
### What Communicates With What
|
|
|
|
| Boundary | Communication Method | Direction | Notes |
|
|
|----------|---------------------|-----------|-------|
|
|
| View ↔ ViewModel | WPF data binding (two-way for inputs, one-way for results) | Both | No code-behind |
|
|
| ViewModel ↔ Service | Constructor-injected interface, async method call | VM → Service | Services return Task\<T\> |
|
|
| ViewModel ↔ ViewModel | WeakReferenceMessenger messages | Broadcast | Tenant switch, log events |
|
|
| Service ↔ SessionManager | `GetOrCreateSessionAsync()` | Service → SessionMgr | Every SharePoint call |
|
|
| SessionManager ↔ PnP Framework | `AuthenticationManager.GetContextAsync()` | SessionMgr → PnP | On cache miss only |
|
|
| Service ↔ Graph SDK | `GraphServiceClient` method calls | Service → Graph | For Graph-only operations |
|
|
| SettingsService ↔ FileSystem | `System.Text.Json` + `File.ReadAllText/WriteAllText` | Both | Async I/O |
|
|
| LocalizationService ↔ Views | XAML binding to translated string properties | Service → View | Via singleton binding |
|
|
|
|
### What Must NOT Cross Boundaries
|
|
|
|
- Views must not call services directly — all via ViewModel commands
|
|
- Services must not reference any WPF types (`System.Windows.*`) — use `IProgress<T>` for UI feedback
|
|
- ViewModels must not instantiate `ClientContext` or `AuthenticationManager` directly — only via `IAuthService`
|
|
- SessionManager is the only class that holds `ClientContext` objects — services receive them per-operation
|
|
|
|
---
|
|
|
|
## Build Order (Dependency Graph)
|
|
|
|
The following reflects the order components can be built because later items depend on earlier ones:
|
|
|
|
```
|
|
Phase 1: Foundation
|
|
└── Core/Models/* (no dependencies)
|
|
└── Core/Interfaces/* (no dependencies)
|
|
└── Core/Exceptions/* (no dependencies)
|
|
|
|
Phase 2: Infrastructure Services
|
|
└── SettingsService (depends on Core models)
|
|
└── LocalizationService (depends on lang files)
|
|
└── DialogService (depends on WPF — implement last in phase)
|
|
└── AuthService / SessionManager (depends on PnP Framework NuGet)
|
|
|
|
Phase 3: Feature Services (depend on Auth + Core)
|
|
└── PermissionsService
|
|
└── StorageService
|
|
└── SearchService
|
|
└── TemplateService
|
|
└── DuplicateService
|
|
└── BulkOpsService
|
|
|
|
Phase 4: Reporting (depends on Feature Services output models)
|
|
└── HtmlReportService
|
|
└── CsvExportService
|
|
|
|
Phase 5: ViewModels (depend on service interfaces)
|
|
└── MainWindowViewModel (shell, nav, tenant selector)
|
|
└── Feature ViewModels (Permissions, Storage, Search, Templates, Duplicates, BulkOps)
|
|
└── SettingsViewModel
|
|
|
|
Phase 6: Views + App Bootstrap (depend on ViewModels + DI)
|
|
└── XAML Views (bind to ViewModels)
|
|
└── Controls (TenantSelector, LogPanel, Charts)
|
|
└── App.xaml.cs DI container wiring
|
|
```
|
|
|
|
---
|
|
|
|
## Scaling Considerations
|
|
|
|
This is a local desktop tool with a single user. "Scaling" means handling larger SharePoint tenants, not more users.
|
|
|
|
| Concern | Approach |
|
|
|---------|----------|
|
|
| Large site collections (1000+ sites) | Async streaming with early cancellation; paginated PnP calls; virtual DataGrid |
|
|
| Deep permission hierarchies | Configurable scan depth; user can limit scope to top-level only |
|
|
| Large file search results | Server-side KQL filtering first, client-side regex only as secondary pass |
|
|
| Multiple simultaneous operations | Each ViewModel has its own CancellationTokenSource; operations are isolated |
|
|
| Session token expiry during long scan | MSAL silent refresh + retry on 401; surface error to user if re-auth needed |
|
|
|
|
---
|
|
|
|
## Anti-Patterns
|
|
|
|
### Anti-Pattern 1: `Dispatcher.Invoke` in Services
|
|
|
|
**What people do:** Call `Application.Current.Dispatcher.Invoke()` inside service classes to update UI state.
|
|
**Why it's wrong:** Couples service layer to WPF, makes services untestable, causes deadlocks if called from wrong thread.
|
|
**Do this instead:** Service accepts `IProgress<T>` parameter. `Progress<T>` marshals to UI thread automatically via the captured SynchronizationContext.
|
|
|
|
### Anti-Pattern 2: Giant "God ViewModel"
|
|
|
|
**What people do:** Create one MainViewModel with all feature logic, mirroring the monolithic PowerShell script.
|
|
**Why it's wrong:** Replicates the exact problem being solved. Hard to navigate, hard to test, merge conflicts on every change.
|
|
**Do this instead:** One ViewModel per feature tab. MainWindowViewModel owns only shell navigation, active tenant, and log state.
|
|
|
|
### Anti-Pattern 3: Storing ClientContext as a Long-Lived Static
|
|
|
|
**What people do:** Cache `ClientContext` in a static field for reuse.
|
|
**Why it's wrong:** `ClientContext` is not thread-safe and has an auth token that expires. Static makes it impossible to manage per-tenant.
|
|
**Do this instead:** `SessionManager` manages ClientContext lifetime. Services request a context per operation. PnP Framework handles token refresh.
|
|
|
|
### Anti-Pattern 4: Blocking Async on Sync Context
|
|
|
|
**What people do:** Call `.Result` or `.Wait()` on Tasks inside WPF event handlers to avoid `async void`.
|
|
**Why it's wrong:** Deadlocks the WPF SynchronizationContext. The UI freezes permanently.
|
|
**Do this instead:** Use `async void` only for top-level event handlers (acceptable in WPF), or bind all user actions to `AsyncRelayCommand`.
|
|
|
|
### Anti-Pattern 5: Silent Catch Blocks (porting the existing bug)
|
|
|
|
**What people do:** Wrap PnP calls in `catch {}` or `catch { /* ignore */ }` to prevent crashes.
|
|
**Why it's wrong:** The existing PowerShell app has 38 such blocks — they produce silent failures, missing data, and phantom "success" states.
|
|
**Do this instead:** Catch specific exceptions (`SharePointException`, `MicrosoftIdentityException`). Log with full stack trace via `ILogger`. Surface user-visible error message via ViewModel's `ErrorMessage` property.
|
|
|
|
---
|
|
|
|
## Integration Points
|
|
|
|
### External Services
|
|
|
|
| Service | Integration Pattern | Library | Notes |
|
|
|---------|---------------------|---------|-------|
|
|
| SharePoint Online (CSOM) | PnP Framework `ClientContext` | `PnP.Framework` NuGet | Use for permissions, storage, templates, bulk ops |
|
|
| SharePoint Search | PnP Framework `SearchRequest` | `PnP.Framework` NuGet | KQL queries; paginated |
|
|
| Microsoft Graph | `GraphServiceClient` | `Microsoft.Graph` NuGet | Use for user/group lookups, Teams data |
|
|
| Azure AD / MSAL | `PublicClientApplication` via PnP `AuthenticationManager` | Built into `PnP.Framework` | Interactive browser login; token cache callback |
|
|
| WPF Charts | `LiveCharts2` or `OxyPlot.Wpf` | NuGet | Storage metrics visualization; LiveCharts2 preferred for richer WPF binding |
|
|
|
|
### Internal Boundaries
|
|
|
|
| Boundary | Communication | Notes |
|
|
|----------|---------------|-------|
|
|
| SessionManager ↔ Feature Services | `TenantSession` passed per operation | Services do not store sessions |
|
|
| LocalizationService ↔ XAML | Singleton bound via `StaticResource`; properties fire `INotifyPropertyChanged` on language switch | All UI text goes through this |
|
|
| ReportExportService ↔ ViewModels | Called after operation completes; returns file path | Self-contained HTML with embedded JS/CSS |
|
|
| SettingsService ↔ all singletons | Read at startup; written on change | JSON format must match existing `Sharepoint_Settings.json` schema for migration |
|
|
|
|
---
|
|
|
|
## Sources
|
|
|
|
- [Introduction to MVVM Toolkit - Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/) — HIGH confidence
|
|
- [AsyncRelayCommand - CommunityToolkit](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/asyncrelaycommand) — HIGH confidence
|
|
- [PnP Framework AuthenticationManager API](https://pnp.github.io/pnpframework/api/PnP.Framework.AuthenticationManager.html) — HIGH confidence
|
|
- [PnP Framework Getting Started](https://pnp.github.io/pnpframework/using-the-framework/readme.html) — HIGH confidence
|
|
- [Acquire and cache tokens with MSAL - Microsoft Learn](https://learn.microsoft.com/en-us/entra/identity-platform/msal-acquire-cache-tokens) — HIGH confidence
|
|
- [WPF Development Best Practices 2024 - MESCIUS](https://medium.com/mesciusinc/wpf-development-best-practices-for-2024-9e5062c71350) — MEDIUM confidence
|
|
- [Modern WPF Development: MVVM and Prism - Einfochips](https://www.einfochips.com/blog/modern-wpf-development-leveraging-mvvm-and-prism-for-enterprise-app/) — MEDIUM confidence
|
|
- [Async Programming Patterns for MVVM - Microsoft Learn](https://learn.microsoft.com/en-us/archive/msdn-magazine/2014/april/async-programming-patterns-for-asynchronous-mvvm-applications-commands) — HIGH confidence
|
|
|
|
---
|
|
|
|
*Architecture research for: C#/WPF SharePoint Online administration desktop tool*
|
|
*Researched: 2026-04-02*
|