Skip to content

Commit e0e0d96

Browse files
CopilotJusterZhu
andauthored
Refactor Extension package builder: add error feedback, fix async patterns, and fix critical ViewModel bug (#8)
* Initial plan * Refactor ExtensionViewModel: Add error handling, validation, and user feedback Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> * Fix async consistency in MessageBox calls Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> * Convert commands to AsyncRelayCommand for proper async/await pattern Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> * Remove redundant operations and optimize JSON serialization Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> * Final polish: improve JSON settings, formatting, and success message UX Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> * Fix critical bug: Initialize ExtensionViewModel DataContext in ExtensionView Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com>
1 parent 714a5e0 commit e0e0d96

2 files changed

Lines changed: 91 additions & 33 deletions

File tree

src/ViewModels/ExtensionViewModel.cs

Lines changed: 89 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using GeneralUpdate.Tool.Avalonia.Common;
1010
using GeneralUpdate.Tool.Avalonia.Models;
1111
using Newtonsoft.Json;
12+
using Nlnet.Avalonia.Controls;
1213

1314
namespace GeneralUpdate.Tool.Avalonia.ViewModels;
1415

@@ -19,13 +20,13 @@ public class ExtensionViewModel : ObservableObject
1920

2021
private ExtensionConfigModel? _configModel;
2122
private AsyncRelayCommand? _generateCommand;
22-
private AsyncRelayCommand<string>? _selectFolderCommand;
23+
private AsyncRelayCommand<string?>? _selectFolderCommand;
2324
private RelayCommand? _loadedCommand;
2425
private RelayCommand? _clearCommand;
2526
private AsyncRelayCommand? _selectDependenciesCommand;
2627
private ExtensionDependencySelectionModel? _selectedDependency;
27-
private RelayCommand<CustomPropertyModel>? _removeCustomPropertyCommand;
28-
private RelayCommand? _addCustomPropertyCommand;
28+
private AsyncRelayCommand<CustomPropertyModel>? _removeCustomPropertyCommand;
29+
private AsyncRelayCommand? _addCustomPropertyCommand;
2930
private string? _newCustomPropertyKey;
3031
private string? _newCustomPropertyValue;
3132

@@ -38,9 +39,9 @@ public RelayCommand LoadedCommand
3839
get { return _loadedCommand ??= new RelayCommand(LoadedAction); }
3940
}
4041

41-
public AsyncRelayCommand<string> SelectFolderCommand
42+
public AsyncRelayCommand<string?> SelectFolderCommand
4243
{
43-
get => _selectFolderCommand ??= new AsyncRelayCommand<string>(SelectFolderAction);
44+
get => _selectFolderCommand ??= new AsyncRelayCommand<string?>(SelectFolderAction);
4445
}
4546

4647
public AsyncRelayCommand GenerateCommand
@@ -76,22 +77,25 @@ public ExtensionDependencySelectionModel? SelectedDependency
7677
set => SetProperty(ref _selectedDependency, value);
7778
}
7879

79-
public RelayCommand<CustomPropertyModel> RemoveCustomPropertyCommand
80+
public AsyncRelayCommand<CustomPropertyModel> RemoveCustomPropertyCommand
8081
{
81-
get => _removeCustomPropertyCommand ??= new RelayCommand<CustomPropertyModel>(RemoveCustomPropertyAction);
82+
get => _removeCustomPropertyCommand ??= new AsyncRelayCommand<CustomPropertyModel>(RemoveCustomPropertyAction);
8283
}
8384

84-
public RelayCommand AddCustomPropertyCommand
85+
public AsyncRelayCommand AddCustomPropertyCommand
8586
{
86-
get => _addCustomPropertyCommand ??= new RelayCommand(AddCustomPropertyAction, CanAddCustomProperty);
87+
get => _addCustomPropertyCommand ??= new AsyncRelayCommand(AddCustomPropertyAction, CanAddCustomProperty);
8788
}
8889

8990
public string? NewCustomPropertyKey
9091
{
9192
get => _newCustomPropertyKey;
9293
set
9394
{
94-
SetProperty(ref _newCustomPropertyKey, value);
95+
if (SetProperty(ref _newCustomPropertyKey, value))
96+
{
97+
AddCustomPropertyCommand.NotifyCanExecuteChanged();
98+
}
9599
}
96100
}
97101

@@ -100,7 +104,10 @@ public string? NewCustomPropertyValue
100104
get => _newCustomPropertyValue;
101105
set
102106
{
103-
SetProperty(ref _newCustomPropertyValue, value);
107+
if (SetProperty(ref _newCustomPropertyValue, value))
108+
{
109+
AddCustomPropertyCommand.NotifyCanExecuteChanged();
110+
}
104111
}
105112
}
106113

@@ -159,26 +166,42 @@ private void ResetAction()
159166
NewCustomPropertyValue = string.Empty;
160167
}
161168

162-
private async Task SelectFolderAction(string value)
169+
private async Task SelectFolderAction(string? value)
163170
{
164171
try
165172
{
173+
if (string.IsNullOrWhiteSpace(value))
174+
{
175+
await MessageBox.ShowAsync("Invalid folder selection parameter", "Error", Buttons.OK);
176+
return;
177+
}
178+
166179
var folders = await Storage.Instance.SelectFolderDialog();
167180
if (!folders.Any()) return;
168181

169182
var folder = folders.First();
183+
if (folder?.Path?.LocalPath == null)
184+
{
185+
await MessageBox.ShowAsync("Selected folder path is invalid", "Error", Buttons.OK);
186+
return;
187+
}
188+
170189
switch (value)
171190
{
172191
case "ExtensionDirectory":
173-
ConfigModel.ExtensionDirectory = folder!.Path.LocalPath;
192+
ConfigModel.ExtensionDirectory = folder.Path.LocalPath;
174193
break;
175194
case "ExportPath":
176-
ConfigModel.Path = folder!.Path.LocalPath;
195+
ConfigModel.Path = folder.Path.LocalPath;
196+
break;
197+
default:
198+
await MessageBox.ShowAsync($"Unknown folder selection type: {value}", "Error", Buttons.OK);
177199
break;
178200
}
179201
}
180-
catch (Exception e)
202+
catch (Exception ex)
181203
{
204+
await MessageBox.ShowAsync($"Failed to select folder: {ex.Message}", "Error", Buttons.OK);
182205
}
183206
}
184207

@@ -192,26 +215,31 @@ private async Task GeneratePackageAction()
192215
// Validate input
193216
if (string.IsNullOrWhiteSpace(ConfigModel.Name))
194217
{
195-
//eventAggregator.PublishWarning("Extension name is required");
218+
await MessageBox.ShowAsync("Extension name is required", "Validation Error", Buttons.OK);
196219
return;
197220
}
198221

199222
if (string.IsNullOrWhiteSpace(ConfigModel.Version))
200223
{
201-
//eventAggregator.PublishWarning("Extension version is required");
224+
await MessageBox.ShowAsync("Extension version is required", "Validation Error", Buttons.OK);
202225
return;
203226
}
204227

205-
if (string.IsNullOrWhiteSpace(ConfigModel.ExtensionDirectory) ||
206-
!Directory.Exists(ConfigModel.ExtensionDirectory))
228+
if (string.IsNullOrWhiteSpace(ConfigModel.ExtensionDirectory))
207229
{
208-
//eventAggregator.PublishWarning("Extension directory is invalid");
230+
await MessageBox.ShowAsync("Extension directory is required", "Validation Error", Buttons.OK);
231+
return;
232+
}
233+
234+
if (!Directory.Exists(ConfigModel.ExtensionDirectory))
235+
{
236+
await MessageBox.ShowAsync($"Extension directory does not exist: {ConfigModel.ExtensionDirectory}", "Validation Error", Buttons.OK);
209237
return;
210238
}
211239

212240
if (string.IsNullOrWhiteSpace(ConfigModel.Path))
213241
{
214-
//eventAggregator.PublishWarning("Export path is required");
242+
await MessageBox.ShowAsync("Export path is required", "Validation Error", Buttons.OK);
215243
return;
216244
}
217245

@@ -232,8 +260,6 @@ private async Task GeneratePackageAction()
232260
var zipFileName = $"{sanitizedName}_{sanitizedVersion}.zip";
233261
var zipFilePath = Path.Combine(exportDirectory, zipFileName);
234262

235-
//eventAggregator.PublishSuccess("Starting extension compression...");
236-
237263
// Compress the extension directory into a zip file
238264
await ZipUtility.CompressDirectoryAsync(
239265
ConfigModel.ExtensionDirectory,
@@ -247,20 +273,42 @@ await ZipUtility.CompressDirectoryAsync(
247273
// Create manifest.json with all ExtensionDTO fields
248274
var platformValue = ConfigModel.Platform?.Value ?? 0;
249275
var targetPlatform = MapPlatformValue(platformValue);
250-
ConfigModel.Platform = new PlatformModel{ DisplayName = targetPlatform.ToString(), Value = platformValue };
276+
ConfigModel.Platform = new PlatformModel { DisplayName = targetPlatform.ToString(), Value = platformValue };
277+
251278
// Get file info for the zip
252279
var fileInfo = new FileInfo(zipFilePath);
253280
ConfigModel.FileSize = fileInfo.Length;
254-
// Serialize manifest to JSON
255-
var manifestJson = JsonConvert.SerializeObject(ConfigModel);
281+
282+
// Serialize manifest to JSON with explicit settings
283+
var jsonSettings = new JsonSerializerSettings
284+
{
285+
NullValueHandling = NullValueHandling.Ignore
286+
};
287+
var manifestJson = JsonConvert.SerializeObject(ConfigModel, jsonSettings);
256288
if (!string.IsNullOrEmpty(manifestJson))
257289
{
258290
// Add manifest.json to the zip file
259291
await ZipUtility.AddFileToZipAsync(zipFilePath, "manifest.json", manifestJson);
260292
}
293+
294+
var fileName = Path.GetFileName(zipFilePath);
295+
var directory = Path.GetDirectoryName(zipFilePath);
296+
await MessageBox.ShowAsync(
297+
$"Extension package created successfully:\n\nFile: {fileName}\nLocation: {directory}",
298+
"Success",
299+
Buttons.OK);
300+
}
301+
catch (UnauthorizedAccessException ex)
302+
{
303+
await MessageBox.ShowAsync($"Access denied: {ex.Message}\nPlease check file permissions.", "Error", Buttons.OK);
304+
}
305+
catch (IOException ex)
306+
{
307+
await MessageBox.ShowAsync($"I/O error: {ex.Message}", "Error", Buttons.OK);
261308
}
262309
catch (Exception ex)
263310
{
311+
await MessageBox.ShowAsync($"Failed to generate package: {ex.Message}", "Error", Buttons.OK);
264312
}
265313
}
266314

@@ -272,19 +320,26 @@ private bool CanAddCustomProperty()
272320
!string.IsNullOrWhiteSpace(NewCustomPropertyValue);
273321
}
274322

275-
private void AddCustomPropertyAction()
323+
private async Task AddCustomPropertyAction()
276324
{
277325
try
278326
{
279-
if (string.IsNullOrWhiteSpace(NewCustomPropertyKey) ||
280-
string.IsNullOrWhiteSpace(NewCustomPropertyValue))
327+
if (string.IsNullOrWhiteSpace(NewCustomPropertyKey))
328+
{
329+
await MessageBox.ShowAsync("Property key cannot be empty", "Validation Error", Buttons.OK);
330+
return;
331+
}
332+
333+
if (string.IsNullOrWhiteSpace(NewCustomPropertyValue))
281334
{
335+
await MessageBox.ShowAsync("Property value cannot be empty", "Validation Error", Buttons.OK);
282336
return;
283337
}
284338

285339
// Check if key already exists
286340
if (ConfigModel.CustomProperties.ContainsKey(NewCustomPropertyKey))
287341
{
342+
await MessageBox.ShowAsync($"Property key '{NewCustomPropertyKey}' already exists", "Validation Error", Buttons.OK);
288343
return;
289344
}
290345

@@ -301,34 +356,35 @@ private void AddCustomPropertyAction()
301356
// Clear input fields
302357
NewCustomPropertyKey = string.Empty;
303358
NewCustomPropertyValue = string.Empty;
304-
305359
}
306360
catch (Exception ex)
307361
{
362+
await MessageBox.ShowAsync($"Failed to add custom property: {ex.Message}", "Error", Buttons.OK);
308363
}
309364
}
310365

311-
private void RemoveCustomPropertyAction(CustomPropertyModel? property)
366+
private async Task RemoveCustomPropertyAction(CustomPropertyModel? property)
312367
{
313368
try
314369
{
315370
if (property == null)
316371
{
372+
await MessageBox.ShowAsync("No property selected to remove", "Validation Error", Buttons.OK);
317373
return;
318374
}
319375

320-
// Remove from dictionary - use TryGetValue for safety
376+
// Remove from dictionary
321377
if (ConfigModel.CustomProperties.ContainsKey(property.Key))
322378
{
323379
ConfigModel.CustomProperties.Remove(property.Key);
324380
}
325381

326382
// Remove from observable collection
327383
CustomPropertiesCollection.Remove(property);
328-
329384
}
330385
catch (Exception ex)
331386
{
387+
await MessageBox.ShowAsync($"Failed to remove custom property: {ex.Message}", "Error", Buttons.OK);
332388
}
333389
}
334390

src/Views/ExtensionView.axaml.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Avalonia;
22
using Avalonia.Controls;
33
using Avalonia.Markup.Xaml;
4+
using GeneralUpdate.Tool.Avalonia.ViewModels;
45

56
namespace GeneralUpdate.Tool.Avalonia.Views;
67

@@ -9,5 +10,6 @@ public partial class ExtensionView : UserControl
910
public ExtensionView()
1011
{
1112
InitializeComponent();
13+
DataContext = new ExtensionViewModel();
1214
}
1315
}

0 commit comments

Comments
 (0)