Modernize DockerRegistryExplorer sample to .NET 8.0#31
Modernize DockerRegistryExplorer sample to .NET 8.0#31
Conversation
Closes #25 - Convert project from .NET Framework 4.8 to modern SDK-style targeting net8.0-windows - Update all NuGet packages to latest versions: - Autofac: 6.4.0 → 8.4.0 - CommunityToolkit.Mvvm: 8.0.0 → 8.4.0 - Newtonsoft.Json: 13.0.1 → 13.0.4 - Serilog: 2.12.0 → 4.3.0 - Serilog.Sinks.Console: 4.1.0 → 6.0.0 - Serilog.Sinks.Debug: 2.0.0 → 3.0.0 - Replace obsolete MvvmLight with CommunityToolkit.Mvvm: - Migrate all ViewModels from ViewModelBase to ObservableObject - Replace RelayCommand with CommunityToolkit.Mvvm.Input.RelayCommand - Update RaisePropertyChanged() to OnPropertyChanged() - Remove deprecated DispatcherHelper.Initialize() - Update API calls for new Docker.Registry.DotNet conventions: - Remove "Async" suffix from method names - Use ImageReference.Create() for manifest operations - Extract ImageTag.Value when passing tags as strings - Fix default Docker Hub endpoint to https://registry-1.docker.io - Enable nullable reference types and implicit usings - Disable auto-generated AssemblyInfo to prevent conflicts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
WalkthroughModernized the DockerRegistryExplorer sample to SDK-style .NET (net8.0-windows), migrated MVVM Light to CommunityToolkit.ObservableObject, updated DI/startup and global usings, adjusted namespaces/usings and XAML aliases, refined authentication/client flows, added Task extension, and reworked manifest/tag viewmodel command workflows. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant ConnectVM as ConnectViewModel
participant Config as RegistryClientConfiguration
participant Client as IRegistryClient
User->>ConnectVM: Click Connect
ConnectVM->>Config: Build configuration (endpoint)
alt Username/Password provided
ConnectVM->>Config: UsePasswordOAuthAuthentication(username, password)
end
ConnectVM->>Client: CreateClient()
ConnectVM->>Client: Ping()
Client-->>ConnectVM: Result (success/error)
ConnectVM-->>User: Update UI / show message
sequenceDiagram
autonumber
actor User
participant TagVM as TagViewModel
participant Exec as AsyncExecutor
participant Reg as IRegistryClient
participant ViewSvc as IViewService
User->>TagVM: ViewManifestCommand
TagVM->>Exec: ExecuteAsync(GetManifestInternal)
Exec->>Reg: GetManifest(repository, tag)
Reg-->>Exec: Manifest or Error
alt Error
Exec-->>TagVM: ex
TagVM-->>User: Show error
else Manifest
Exec-->>TagVM: manifest
alt Text/JSON
TagVM->>ViewSvc: Show TextDialog
else Structured
TagVM->>ViewSvc: Show ManifestDialog
end
end
sequenceDiagram
autonumber
actor User
participant MVM as ManifestDialogViewModel
participant Exec as AsyncExecutor
participant Reg as IRegistryClient
participant FileDlg as IFileDialogService
participant FS as FileSystem
User->>MVM: DownloadCommand
MVM->>Exec: ExecuteAsync(GetBlob)
Exec->>Reg: GetBlob(digest)
Reg-->>Exec: Stream or Error
alt Error
Exec-->>MVM: ex
MVM-->>User: Show error
else Success
Exec-->>MVM: blobStream
MVM->>FileDlg: ShowSaveFileDialog()
FileDlg-->>MVM: Path or cancel
alt Path chosen
MVM->>FS: Create file stream
MVM->>FS: Copy blob -> file
MVM-->>User: "Layer saved"
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🔇 Additional comments (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Test Results12 tests 12 ✅ 0s ⏱️ Results for commit 6dcf2e4. ♻️ This comment has been updated with latest results. |
The DockerRegistryExplorer WPF sample project requires Windows to build. Updated the test workflow to only run tests on the test project instead of all projects in the solution to avoid building Windows-only samples on Linux. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
samples/DockerRegistryExplorer/ViewModel/ConnectViewModel.cs (1)
76-79: Consider validating for empty strings in addition to null.The authentication condition checks for null but allows empty strings, which would trigger an authentication attempt with invalid credentials rather than connecting anonymously.
Apply this diff to improve the validation:
-if (!this.IsAnonymous && this.Username != null && this.Password != null) +if (!this.IsAnonymous && !string.IsNullOrWhiteSpace(this.Username) && !string.IsNullOrWhiteSpace(this.Password)) configuration.UsePasswordOAuthAuthentication(this.Username, this.Password);samples/DockerRegistryExplorer/ViewModel/MainViewModel.cs (1)
13-20: Consider adding null check for lifetimeScope parameter.Line 16 validates
viewServicewith a null check, but line 15 assignslifetimeScopewithout validation. For consistency and defensive programming, consider adding the same null guard.Apply this diff to add consistent null checking:
public MainViewModel(ILifetimeScope lifetimeScope, IViewService viewService) { - _lifetimeScope = lifetimeScope; + _lifetimeScope = lifetimeScope ?? throw new ArgumentNullException(nameof(lifetimeScope)); _viewService = viewService ?? throw new ArgumentNullException(nameof(viewService));samples/DockerRegistryExplorer/TaskExtensions.cs (1)
5-7: Consider logging unobserved exceptions.The
IgnoreAsyncextension method implements fire-and-forget, but unobserved task exceptions can crash the app in some scenarios. Consider observing exceptions for logging even if you don't handle them.Apply this diff to add basic exception observation:
- public static void IgnoreAsync(this Task task) + public static async void IgnoreAsync(this Task task) { + try + { + await task.ConfigureAwait(false); + } + catch (Exception ex) + { + // Log but don't propagate - this is fire-and-forget + System.Diagnostics.Debug.WriteLine($"Unobserved exception in IgnoreAsync: {ex}"); + } }samples/DockerRegistryExplorer/BuilderExtensions.cs (1)
10-10: Typo in comment: "Registry" → "Register".Apply this diff:
- //Registry the view model itself + //Register the view model itself builder.RegisterType<TViewModel>();
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (28)
.gitignore(1 hunks)samples/DockerRegistryExplorer/App.xaml.cs(1 hunks)samples/DockerRegistryExplorer/BuilderExtensions.cs(1 hunks)samples/DockerRegistryExplorer/ContainerFactory.cs(1 hunks)samples/DockerRegistryExplorer/Controls/BusyIndicator.xaml(1 hunks)samples/DockerRegistryExplorer/Controls/BusyIndicator.xaml.cs(1 hunks)samples/DockerRegistryExplorer/Converters/InverseBooleanConverter.cs(1 hunks)samples/DockerRegistryExplorer/DockerRegistryExplorer.csproj(1 hunks)samples/DockerRegistryExplorer/GlobalUsings.cs(1 hunks)samples/DockerRegistryExplorer/Properties/AssemblyInfo.cs(0 hunks)samples/DockerRegistryExplorer/TaskExtensions.cs(1 hunks)samples/DockerRegistryExplorer/View/ConnectView.xaml(2 hunks)samples/DockerRegistryExplorer/View/ConnectView.xaml.cs(0 hunks)samples/DockerRegistryExplorer/View/MainWindow.xaml.cs(0 hunks)samples/DockerRegistryExplorer/View/ManfiestDialogView.xaml.cs(0 hunks)samples/DockerRegistryExplorer/View/TextDialogView.xaml.cs(0 hunks)samples/DockerRegistryExplorer/ViewModel/AsyncExecutor.cs(2 hunks)samples/DockerRegistryExplorer/ViewModel/CloseableViewModelBase.cs(1 hunks)samples/DockerRegistryExplorer/ViewModel/ConnectViewModel.cs(6 hunks)samples/DockerRegistryExplorer/ViewModel/DialogViewModelBase.cs(1 hunks)samples/DockerRegistryExplorer/ViewModel/MainViewModel.cs(1 hunks)samples/DockerRegistryExplorer/ViewModel/ManifestDialogViewModel.cs(1 hunks)samples/DockerRegistryExplorer/ViewModel/ManifestLayerViewModel.cs(1 hunks)samples/DockerRegistryExplorer/ViewModel/RegistryViewModel.cs(1 hunks)samples/DockerRegistryExplorer/ViewModel/RepositoriesViewModel.cs(1 hunks)samples/DockerRegistryExplorer/ViewModel/RepositoryViewModel.cs(1 hunks)samples/DockerRegistryExplorer/ViewModel/TagViewModel.cs(1 hunks)samples/DockerRegistryExplorer/ViewModel/TextDialogViewModel.cs(1 hunks)
💤 Files with no reviewable changes (5)
- samples/DockerRegistryExplorer/Properties/AssemblyInfo.cs
- samples/DockerRegistryExplorer/View/MainWindow.xaml.cs
- samples/DockerRegistryExplorer/View/ConnectView.xaml.cs
- samples/DockerRegistryExplorer/View/TextDialogView.xaml.cs
- samples/DockerRegistryExplorer/View/ManfiestDialogView.xaml.cs
🧰 Additional context used
🧬 Code graph analysis (9)
samples/DockerRegistryExplorer/ViewModel/ManifestLayerViewModel.cs (1)
samples/DockerRegistryExplorer/ViewModel/AsyncExecutor.cs (1)
AsyncExecutor(10-56)
samples/DockerRegistryExplorer/ViewModel/RegistryViewModel.cs (3)
samples/DockerRegistryExplorer/ViewModel/RepositoriesViewModel.cs (3)
RepositoriesViewModel(9-114)RepositoriesViewModel(21-37)Refresh(106-108)samples/DockerRegistryExplorer/ViewModel/MainViewModel.cs (1)
Refresh(52-55)samples/DockerRegistryExplorer/ViewModel/RepositoryViewModel.cs (3)
Refresh(47-52)RepositoryViewModel(5-74)RepositoryViewModel(13-27)
samples/DockerRegistryExplorer/ViewModel/ManifestDialogViewModel.cs (4)
samples/DockerRegistryExplorer/ViewModel/CloseableViewModelBase.cs (1)
CloseableViewModelBase(6-23)samples/DockerRegistryExplorer/ViewModel/TagViewModel.cs (2)
TagViewModel(6-168)TagViewModel(18-41)samples/DockerRegistryExplorer/ViewModel/ManifestLayerViewModel.cs (2)
ManifestLayerViewModel(5-30)ManifestLayerViewModel(13-21)samples/DockerRegistryExplorer/ViewModel/AsyncExecutor.cs (1)
AsyncExecutor(10-56)
samples/DockerRegistryExplorer/ContainerFactory.cs (1)
samples/DockerRegistryExplorer/BuilderExtensions.cs (1)
RegisterViewModel(7-15)
samples/DockerRegistryExplorer/ViewModel/TagViewModel.cs (4)
samples/DockerRegistryExplorer/ViewModel/RepositoryViewModel.cs (3)
RepositoryViewModel(5-74)RepositoryViewModel(13-27)Task(54-68)samples/DockerRegistryExplorer/ViewModel/AsyncExecutor.cs (2)
AsyncExecutor(10-56)Task(26-48)samples/DockerRegistryExplorer/ViewModel/TextDialogViewModel.cs (2)
TextDialogViewModel(3-14)TextDialogViewModel(5-9)samples/DockerRegistryExplorer/ViewModel/ManifestDialogViewModel.cs (2)
ManifestDialogViewModel(8-112)ManifestDialogViewModel(24-51)
samples/DockerRegistryExplorer/ViewModel/RepositoryViewModel.cs (5)
samples/DockerRegistryExplorer/ViewModel/TagViewModel.cs (3)
TagViewModel(6-168)TagViewModel(18-41)Task(86-101)samples/DockerRegistryExplorer/ViewModel/RegistryViewModel.cs (3)
RegistryViewModel(3-28)RegistryViewModel(5-16)Refresh(24-27)samples/DockerRegistryExplorer/ViewModel/RepositoriesViewModel.cs (3)
Refresh(106-108)Task(62-76)Task(85-104)samples/DockerRegistryExplorer/ViewModel/AsyncExecutor.cs (2)
AsyncExecutor(10-56)Task(26-48)samples/DockerRegistryExplorer/TaskExtensions.cs (1)
IgnoreAsync(5-7)
samples/DockerRegistryExplorer/ViewModel/RepositoriesViewModel.cs (4)
samples/DockerRegistryExplorer/ViewModel/RegistryViewModel.cs (3)
RegistryViewModel(3-28)RegistryViewModel(5-16)Refresh(24-27)samples/DockerRegistryExplorer/ViewModel/RepositoryViewModel.cs (5)
RepositoryViewModel(5-74)RepositoryViewModel(13-27)Task(54-68)Refresh(47-52)CanRefresh(70-73)samples/DockerRegistryExplorer/ViewModel/AsyncExecutor.cs (2)
AsyncExecutor(10-56)Task(26-48)samples/DockerRegistryExplorer/TaskExtensions.cs (1)
IgnoreAsync(5-7)
samples/DockerRegistryExplorer/ViewModel/MainViewModel.cs (4)
samples/DockerRegistryExplorer/ViewModel/RegistryViewModel.cs (3)
RegistryViewModel(3-28)RegistryViewModel(5-16)Refresh(24-27)samples/DockerRegistryExplorer/ViewModel/RepositoryViewModel.cs (1)
Refresh(47-52)samples/DockerRegistryExplorer/ViewModel/AsyncExecutor.cs (1)
AsyncExecutor(10-56)samples/DockerRegistryExplorer/ViewModel/ConnectViewModel.cs (1)
ConnectViewModel(8-87)
samples/DockerRegistryExplorer/ViewModel/DialogViewModelBase.cs (2)
samples/DockerRegistryExplorer/ViewModel/CloseableViewModelBase.cs (2)
CloseableViewModelBase(6-23)RaiseCloseEventArgs(17-20)samples/DockerRegistryExplorer/ViewModel/ConnectViewModel.cs (1)
Ok(67-72)
🪛 GitHub Actions: Build and Push to Nuget
samples/DockerRegistryExplorer/DockerRegistryExplorer.csproj
[error] 107-107: NETSDK1100: To build a project targeting Windows on this operating system, set the EnableWindowsTargeting property to true. (Triggered during: dotnet test --logger trx --collect:"XPlat Code Coverage")
🔇 Additional comments (18)
.gitignore (1)
289-289: Local settings ignore looks good.Excluding
settings.local.jsonkeeps per-developer configuration and secrets out of source control.samples/DockerRegistryExplorer/Controls/BusyIndicator.xaml.cs (1)
1-3: LGTM! Clean namespace migration.The namespace update from
DockerExplorer.ControlstoDockerRegistryExplorer.Controlsis consistent with the project modernization, and the reduction of using directives aligns with implicit usings enabled in .NET 8.samples/DockerRegistryExplorer/View/ConnectView.xaml (1)
13-13: LGTM! Namespace alias correctly updated.The addition of the
xmlns:Controlsnamespace alias and the updatedBusyIndicatorreference align perfectly with the namespace changes in the control's code-behind file.Also applies to: 111-111
samples/DockerRegistryExplorer/Controls/BusyIndicator.xaml (1)
1-1: LGTM! x:Class attribute correctly updated.The
x:Classattribute update matches the namespace change in the code-behind file (BusyIndicator.xaml.cs), ensuring proper XAML-to-code binding.samples/DockerRegistryExplorer/ViewModel/ConnectViewModel.cs (3)
4-4: LGTM!The import path update to
Docker.Registry.DotNet.Application.Authenticationand the corrected Docker Hub endpoint (https://registry-1.docker.io) align with the modernization objectives.Also applies to: 10-10
22-60: LGTM!The property notification updates from
RaisePropertyChanged()toOnPropertyChanged()correctly implement the migration from MVVM Light to CommunityToolkit.Mvvm.
81-85: LGTM!The updated API usage—
CreateClient()andPing()without the "Async" suffix—correctly follows the Docker.Registry.DotNet conventions.samples/DockerRegistryExplorer/ViewModel/MainViewModel.cs (2)
5-11: LGTM! Clean migration to CommunityToolkit.Mvvm.The change from
ViewModelBasetoObservableObjectaligns with the migration from MvvmLight to CommunityToolkit.Mvvm. The inline field initialization using target-typednew()expressions is modern and concise.
52-55: LGTM!The refresh logic correctly iterates over all registries and invokes their
Refresh()methods. This is clean and straightforward.samples/DockerRegistryExplorer/DockerRegistryExplorer.csproj (2)
16-16: Cas.Common.WPF compatibility acknowledged.As noted in the PR summary, Cas.Common.WPF v1.0.42 is a .NET Framework package and may emit compatibility warnings. Consider replacing it with a .NET-compatible alternative in a future update.
11-11: Assembly metadata provided manually
A manual AssemblyInfo.cs exists at samples/DockerRegistryExplorer/Properties/AssemblyInfo.cs, so disabling GenerateAssemblyInfo is safe.samples/DockerRegistryExplorer/Converters/InverseBooleanConverter.cs (1)
1-31: LGTM! Modernization changes applied correctly.The file has been successfully modernized with file-scoped namespace, explicit using for
System.Globalization, and minor formatting improvements. No functional changes, and the implementation remains correct.samples/DockerRegistryExplorer/App.xaml.cs (1)
10-20: LGTM! Correct migration from MvvmLight dispatcher initialization.The removal of
DispatcherHelper.Initialize()is appropriate for the migration to CommunityToolkit.Mvvm, which doesn't require manual dispatcher initialization. The container lifecycle management withusingis correct for the modal dialog pattern.samples/DockerRegistryExplorer/GlobalUsings.cs (1)
1-26: LGTM! Global usings well-chosen for WPF app.The global using directives appropriately consolidate common namespaces for WPF, MVVM, DI, and the Docker Registry API. This aligns well with the
ImplicitUsings=enablesetting in the project file.samples/DockerRegistryExplorer/ViewModel/AsyncExecutor.cs (2)
10-10: LGTM! Correct migration to ObservableObject.The base class migration from
ViewModelBasetoObservableObjectis correct for the CommunityToolkit.Mvvm migration.
22-22: LGTM! Correct property notification method.The change from
RaisePropertyChanged()toOnPropertyChanged()is the correct equivalent in CommunityToolkit.Mvvm.samples/DockerRegistryExplorer/BuilderExtensions.cs (1)
1-16: LGTM! DI registration logic is correct.The file-scoped namespace modernization and DI registration pattern are appropriate for the migration.
samples/DockerRegistryExplorer/ViewModel/CloseableViewModelBase.cs (1)
6-6: LGTM! Correct base class migration.The change from
ViewModelBasetoObservableObjectaligns with the CommunityToolkit.Mvvm migration while preserving theICloseableViewModelinterface contract.
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <OutputType>WinExe</OutputType> | ||
| <TargetFramework>net8.0-windows</TargetFramework> | ||
| <UseWPF>true</UseWPF> | ||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <LangVersion>latest</LangVersion> | ||
| <AssemblyName>DockerRegistryExplorer</AssemblyName> | ||
| <RootNamespace>DockerRegistryExplorer</RootNamespace> | ||
| <GenerateAssemblyInfo>false</GenerateAssemblyInfo> | ||
| </PropertyGroup> |
There was a problem hiding this comment.
Pipeline failure: Add EnableWindowsTargeting for non-Windows CI builds.
The pipeline fails with NETSDK1100 because Windows-targeted projects require EnableWindowsTargeting=true on non-Windows build agents. While the app itself requires Windows, enabling this property allows the build system to validate the project structure on Linux/macOS CI runners.
Apply this diff to fix the pipeline failure:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
+ <EnableWindowsTargeting>true</EnableWindowsTargeting>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<AssemblyName>DockerRegistryExplorer</AssemblyName>
<RootNamespace>DockerRegistryExplorer</RootNamespace>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>🤖 Prompt for AI Agents
In samples/DockerRegistryExplorer/DockerRegistryExplorer.csproj around lines 1
to 12, the project targets Windows and fails CI on non-Windows agents; add
<EnableWindowsTargeting>true</EnableWindowsTargeting> inside the existing
PropertyGroup so the SDK will allow Windows-targeted validation on Linux/macOS
runners; update the PropertyGroup to include this property (no other changes).
| private void Connect() | ||
| { | ||
| var viewModel = _lifetimeScope.Resolve<ConnectViewModel>(); | ||
|
|
||
| private void Refresh() | ||
| if (_viewService.ShowDialog(viewModel) ?? false) | ||
| { | ||
| foreach (var registry in Registries) | ||
| var registryClient = viewModel.RegistryClient; | ||
|
|
||
| var childScope = _lifetimeScope.BeginLifetimeScope(builder => | ||
| { | ||
| registry.Refresh(); | ||
| } | ||
| builder.RegisterInstance(registryClient); | ||
| }); | ||
|
|
||
| var registry = childScope.Resolve<RegistryViewModel> | ||
| ( | ||
| new NamedParameter("url", viewModel.Endpoint) | ||
| ); | ||
|
|
||
| _registries.Add(registry); | ||
| } | ||
| } |
There was a problem hiding this comment.
Critical: Child lifetime scope is never disposed.
Lines 38-41 create a child lifetime scope but the childScope variable is lost after the method returns without being disposed. This causes a resource leak because:
- Autofac lifetime scopes implement
IDisposableand should be disposed when no longer needed - Services resolved from the scope (like
RegistryViewModelon lines 43-46) remain tied to that scope - Accumulating undisposed scopes can lead to memory leaks
The scope should be stored alongside each RegistryViewModel and disposed when the registry is removed from the collection or when the application shuts down.
Consider one of these approaches:
Approach 1: Store the scope with the registry
Create a wrapper class:
private class RegistryEntry
{
public RegistryViewModel ViewModel { get; init; }
public ILifetimeScope Scope { get; init; }
}Then modify the code:
-private readonly ObservableCollection<RegistryViewModel> _registries = new();
+private readonly ObservableCollection<RegistryEntry> _registries = new();
-public IEnumerable<RegistryViewModel> Registries => _registries;
+public IEnumerable<RegistryViewModel> Registries => _registries.Select(e => e.ViewModel);
var childScope = _lifetimeScope.BeginLifetimeScope(builder =>
{
builder.RegisterInstance(registryClient);
});
var registry = childScope.Resolve<RegistryViewModel>
(
new NamedParameter("url", viewModel.Endpoint)
);
-_registries.Add(registry);
+_registries.Add(new RegistryEntry { ViewModel = registry, Scope = childScope });And implement proper cleanup when registries are removed.
Approach 2: Use a different scoping strategy
If the registries are meant to live for the application lifetime, consider registering them as singletons in the root container instead of using child scopes.
🤖 Prompt for AI Agents
In samples/DockerRegistryExplorer/ViewModel/MainViewModel.cs around lines 30 to
50, the child ILifetimeScope created for each registry is never disposed causing
resource leaks; fix by associating the scope with the resolved RegistryViewModel
(e.g., create a RegistryEntry wrapper that holds RegistryViewModel and
ILifetimeScope, add RegistryEntry instances to the _registries collection
instead of raw view models), update any UI/collection code to use
RegistryEntry.ViewModel where needed, and ensure you dispose the associated
ILifetimeScope when an entry is removed or when the application shuts down (or
alternatively change registration to register registries as singletons in the
root scope if they should live for the app lifetime).
| using (var stream = response.Stream) | ||
| { | ||
| using (var stream = response.Stream) | ||
| { | ||
| var result = this._fileDialogService.ShowSaveFileDialog(); | ||
| var result = _fileDialogService.ShowSaveFileDialog(); | ||
|
|
||
| if (result != null) | ||
| if (result != null) | ||
| using (var targetStream = File.Create(result.FileName)) | ||
| { | ||
| using (var targetStream = File.Create(result.FileName)) | ||
| { | ||
| await stream.CopyToAsync(targetStream); | ||
| } | ||
| await stream.CopyToAsync(targetStream); | ||
| } | ||
| } | ||
|
|
||
| this._messageBoxService.Show("Layer saved", "Complete"); | ||
| } | ||
| } | ||
|
|
||
| private bool CanDownload() | ||
| { | ||
| return this.SelectedLayer != null; | ||
| _messageBoxService.Show("Layer saved", "Complete"); | ||
| } |
There was a problem hiding this comment.
Show success message only when a file is saved
If the user cancels the save dialog (result == null), we still display “Layer saved”, which is incorrect. Wrap the message inside the “file saved” branch (or return early) so we only signal success when the copy actually happens.
Apply this diff:
- using (var stream = response.Stream)
- {
- var result = _fileDialogService.ShowSaveFileDialog();
-
- if (result != null)
- using (var targetStream = File.Create(result.FileName))
- {
- await stream.CopyToAsync(targetStream);
- }
- }
-
- _messageBoxService.Show("Layer saved", "Complete");
+ using (var stream = response.Stream)
+ {
+ var result = _fileDialogService.ShowSaveFileDialog();
+
+ if (result != null)
+ {
+ using (var targetStream = File.Create(result.FileName))
+ {
+ await stream.CopyToAsync(targetStream);
+ }
+
+ _messageBoxService.Show("Layer saved", "Complete");
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| using (var stream = response.Stream) | |
| { | |
| using (var stream = response.Stream) | |
| { | |
| var result = this._fileDialogService.ShowSaveFileDialog(); | |
| var result = _fileDialogService.ShowSaveFileDialog(); | |
| if (result != null) | |
| if (result != null) | |
| using (var targetStream = File.Create(result.FileName)) | |
| { | |
| using (var targetStream = File.Create(result.FileName)) | |
| { | |
| await stream.CopyToAsync(targetStream); | |
| } | |
| await stream.CopyToAsync(targetStream); | |
| } | |
| } | |
| this._messageBoxService.Show("Layer saved", "Complete"); | |
| } | |
| } | |
| private bool CanDownload() | |
| { | |
| return this.SelectedLayer != null; | |
| _messageBoxService.Show("Layer saved", "Complete"); | |
| } | |
| using (var stream = response.Stream) | |
| { | |
| var result = _fileDialogService.ShowSaveFileDialog(); | |
| if (result != null) | |
| { | |
| using (var targetStream = File.Create(result.FileName)) | |
| { | |
| await stream.CopyToAsync(targetStream); | |
| } | |
| _messageBoxService.Show("Layer saved", "Complete"); | |
| } | |
| } |
🤖 Prompt for AI Agents
In samples/DockerRegistryExplorer/ViewModel/ManifestDialogViewModel.cs around
lines 93 to 105, the success message "_messageBoxService.Show(\"Layer saved\",
\"Complete\");" is shown even when the save dialog was cancelled (result ==
null); modify the flow so the message is only shown after a file was actually
created and the copy finished—either move the _messageBoxService.Show call
inside the if (result != null) block after the await
stream.CopyToAsync(targetStream) or return early when result == null so the
success message is not executed.
| public void Refresh() | ||
| { | ||
| foreach (var child in Children.OfType<RepositoryViewModel>()) child.Refresh(); | ||
| } |
There was a problem hiding this comment.
Fix Refresh child type filtering
Children is populated with RepositoriesViewModel instances, so filtering for RepositoryViewModel yields nothing and the refresh command silently does nothing. Iterate over the correct type so the underlying repositories actually refresh.
Apply this diff:
- foreach (var child in Children.OfType<RepositoryViewModel>()) child.Refresh();
+ foreach (var child in Children.OfType<RepositoriesViewModel>()) child.Refresh();🤖 Prompt for AI Agents
In samples/DockerRegistryExplorer/ViewModel/RegistryViewModel.cs around lines 24
to 27, the Refresh method filters Children for RepositoryViewModel but Children
actually contains RepositoriesViewModel instances, so nothing is refreshed;
change the iteration to Children.OfType<RepositoriesViewModel>() (or the correct
concrete type used when populating Children) and call child.Refresh() for each
so the underlying repositories are properly refreshed.
| var catalog = | ||
| await _registryClient.Catalog.GetCatalog(new CatalogParameters()); | ||
|
|
||
| var repositories = catalog.Repositories.Select( | ||
| r => this._lifetimeScope.Resolve<RepositoryViewModel>( | ||
| new NamedParameter("name", r), | ||
| new TypedParameter(typeof(RegistryViewModel), this._parent))) | ||
| .OrderBy(e => e.Name) | ||
| .ToList(); | ||
| var repositories = catalog.Repositories.Select(r => _lifetimeScope.Resolve<RepositoryViewModel>( | ||
| new NamedParameter("name", r), | ||
| new TypedParameter(typeof(RegistryViewModel), _parent))) | ||
| .OrderBy(e => e.Name) | ||
| .ToList(); | ||
|
|
||
| this.Repositories = new ObservableCollection<RepositoryViewModel>(repositories); | ||
| Repositories = new ObservableCollection<RepositoryViewModel>(repositories); | ||
|
|
||
| Log.Debug("Done Getting Catalog {@Repositories}", repositories); | ||
| } | ||
|
|
||
| private void LoadRepository() | ||
| { | ||
| if (this.Executor.IsBusy) return; | ||
| Log.Debug("Done Getting Catalog {@Repositories}", repositories); |
There was a problem hiding this comment.
Handle null catalog.Repositories before projecting.
Some registries (or transient responses) return a catalog payload with repositories omitted/null. This code will currently throw a NullReferenceException, breaking the refresh flow. Guard the sequence before projecting so the view model can continue with an empty list.
- var repositories = catalog.Repositories.Select(r => _lifetimeScope.Resolve<RepositoryViewModel>(
- new NamedParameter("name", r),
- new TypedParameter(typeof(RegistryViewModel), _parent)))
- .OrderBy(e => e.Name)
- .ToList();
+ var repositoryNames = catalog.Repositories ?? Enumerable.Empty<string>();
+
+ var repositories = repositoryNames
+ .Select(r => _lifetimeScope.Resolve<RepositoryViewModel>(
+ new NamedParameter("name", r),
+ new TypedParameter(typeof(RegistryViewModel), _parent)))
+ .OrderBy(e => e.Name)
+ .ToList();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| var catalog = | |
| await _registryClient.Catalog.GetCatalog(new CatalogParameters()); | |
| var repositories = catalog.Repositories.Select( | |
| r => this._lifetimeScope.Resolve<RepositoryViewModel>( | |
| new NamedParameter("name", r), | |
| new TypedParameter(typeof(RegistryViewModel), this._parent))) | |
| .OrderBy(e => e.Name) | |
| .ToList(); | |
| var repositories = catalog.Repositories.Select(r => _lifetimeScope.Resolve<RepositoryViewModel>( | |
| new NamedParameter("name", r), | |
| new TypedParameter(typeof(RegistryViewModel), _parent))) | |
| .OrderBy(e => e.Name) | |
| .ToList(); | |
| this.Repositories = new ObservableCollection<RepositoryViewModel>(repositories); | |
| Repositories = new ObservableCollection<RepositoryViewModel>(repositories); | |
| Log.Debug("Done Getting Catalog {@Repositories}", repositories); | |
| } | |
| private void LoadRepository() | |
| { | |
| if (this.Executor.IsBusy) return; | |
| Log.Debug("Done Getting Catalog {@Repositories}", repositories); | |
| var catalog = | |
| await _registryClient.Catalog.GetCatalog(new CatalogParameters()); | |
| var repositoryNames = catalog.Repositories ?? Enumerable.Empty<string>(); | |
| var repositories = repositoryNames | |
| .Select(r => _lifetimeScope.Resolve<RepositoryViewModel>( | |
| new NamedParameter("name", r), | |
| new TypedParameter(typeof(RegistryViewModel), _parent))) | |
| .OrderBy(e => e.Name) | |
| .ToList(); | |
| Repositories = new ObservableCollection<RepositoryViewModel>(repositories); | |
| Log.Debug("Done Getting Catalog {@Repositories}", repositories); |
🤖 Prompt for AI Agents
In samples/DockerRegistryExplorer/ViewModel/RepositoriesViewModel.cs around
lines 64 to 75, the code assumes catalog.Repositories is non-null which can
throw a NullReferenceException; fix by guarding the projection with a null check
or null-coalescing fallback (e.g., use catalog.Repositories ??
Enumerable.Empty<string>()), then continue to resolve RepositoryViewModel,
OrderBy, ToList and assign an empty ObservableCollection when there are no
entries; ensure the Log.Debug call receives the resulting repositories list
(which will be an empty list instead of null).
Summary
Modernizes the DockerRegistryExplorer WPF sample application to .NET 8.0 and updates all dependencies to their latest versions.
Closes #25
Changes
Project Modernization
net8.0-windowsGenerateAssemblyInfo=falseto prevent duplicate attribute errorsDependency Updates
Code Modernization
Replaced Obsolete MvvmLight with CommunityToolkit.Mvvm
GalaSoft.MvvmLight.ViewModelBasetoCommunityToolkit.Mvvm.ComponentModel.ObservableObjectGalaSoft.MvvmLight.CommandWpf.RelayCommandwithCommunityToolkit.Mvvm.Input.RelayCommandRaisePropertyChanged()→OnPropertyChanged()DispatcherHelper.Initialize()callUpdated API Calls for New Docker.Registry.DotNet Conventions
GetManifestAsync→GetManifest)ImageReference.Create()wrapperImageTaghandling - extract.Valueproperty when passing tags as stringsOther Improvements
registry.hub.docker.com→registry-1.docker.ioBuild Status
✅ Builds successfully with 0 errors (only nullable reference warnings)
Testing
Tested with:
docker run -d -p 5000:5000 registry:2)Test Plan
Migration Notes
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Chores