docs: update architecture
This commit is contained in:
428
docs/architecture/error-handling.md
Normal file
428
docs/architecture/error-handling.md
Normal file
@@ -0,0 +1,428 @@
|
||||
# Error Handling
|
||||
|
||||
本节定义控件库的错误处理策略,确保异常情况下的稳定性和良好的开发者体验。
|
||||
|
||||
---
|
||||
|
||||
### Error Handling Principles
|
||||
|
||||
**核心原则:**
|
||||
|
||||
1. **Fail Fast(快速失败)**:公开 API 在参数无效时立即抛出异常,不延迟到执行阶段
|
||||
2. **Clear Error Messages(清晰错误信息)**:异常消息应明确指出问题和解决方案
|
||||
3. **No Silent Failures(不吞没异常)**:禁止空 catch 块,必须记录或重新抛出异常
|
||||
4. **Defensive Programming(防御性编程)**:内部方法使用 `Debug.Assert` 验证前置条件
|
||||
|
||||
---
|
||||
|
||||
### Exception Handling Patterns
|
||||
|
||||
**公开 API 参数验证:**
|
||||
|
||||
```csharp
|
||||
namespace Penguin.AvaloniaUI.Themes;
|
||||
|
||||
public static class ThemeManager
|
||||
{
|
||||
/// <summary>
|
||||
/// 应用指定的主题类型
|
||||
/// </summary>
|
||||
/// <param name="type">主题类型</param>
|
||||
/// <exception cref="ArgumentException">当主题类型无效时抛出</exception>
|
||||
public static void ApplyTheme(ThemeType type)
|
||||
{
|
||||
// 参数验证
|
||||
if (!Enum.IsDefined(typeof(ThemeType), type))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Invalid theme type: {type}. Valid values are: {string.Join(", ", Enum.GetNames(typeof(ThemeType)))}",
|
||||
nameof(type));
|
||||
}
|
||||
|
||||
// 核心逻辑
|
||||
try
|
||||
{
|
||||
LoadThemeResources(type);
|
||||
_currentTheme = type;
|
||||
ThemeChanged?.Invoke(null, type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to apply theme '{type}'. Ensure theme resources are available.",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**PropertyGrid 错误处理:**
|
||||
|
||||
```csharp
|
||||
namespace Penguin.AvaloniaUI.Controls.PropertyGrid;
|
||||
|
||||
public class PropertyGrid : TemplatedControl
|
||||
{
|
||||
public static readonly StyledProperty<object?> SelectedObjectProperty =
|
||||
AvaloniaProperty.Register<PropertyGrid, object?>(nameof(SelectedObject));
|
||||
|
||||
public object? SelectedObject
|
||||
{
|
||||
get => GetValue(SelectedObjectProperty);
|
||||
set => SetValue(SelectedObjectProperty, value);
|
||||
}
|
||||
|
||||
private void RefreshProperties(object? obj)
|
||||
{
|
||||
Properties.Clear();
|
||||
|
||||
if (obj == null)
|
||||
{
|
||||
// 空对象是合法的,清空属性列表即可
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var properties = obj.GetType()
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead);
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = CreatePropertyItem(obj, prop);
|
||||
Properties.Add(item);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 单个属性解析失败不应影响其他属性
|
||||
Debug.WriteLine($"[PropertyGrid] Failed to create property item for '{prop.Name}': {ex.Message}");
|
||||
|
||||
// 可选:添加错误占位符
|
||||
Properties.Add(new PropertyItem
|
||||
{
|
||||
Name = prop.Name,
|
||||
DisplayName = $"{prop.Name} (Error)",
|
||||
IsReadOnly = true,
|
||||
Value = $"Error: {ex.Message}"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to parse properties for object of type '{obj.GetType().Name}'.",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
private PropertyItem CreatePropertyItem(object obj, PropertyInfo prop)
|
||||
{
|
||||
Debug.Assert(obj != null, "Object should not be null");
|
||||
Debug.Assert(prop != null, "PropertyInfo should not be null");
|
||||
|
||||
try
|
||||
{
|
||||
var value = prop.GetValue(obj);
|
||||
|
||||
return new PropertyItem
|
||||
{
|
||||
Name = prop.Name,
|
||||
DisplayName = GetDisplayName(prop),
|
||||
Value = value,
|
||||
PropertyType = prop.PropertyType,
|
||||
IsReadOnly = !prop.CanWrite,
|
||||
Category = GetCategory(prop),
|
||||
PropertyInfo = prop
|
||||
};
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Property '{prop.Name}' getter threw an exception.",
|
||||
ex.InnerException ?? ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**UserGuide 错误处理:**
|
||||
|
||||
```csharp
|
||||
namespace Penguin.AvaloniaUI.Controls.UserGuide;
|
||||
|
||||
public class UserGuide : ContentControl
|
||||
{
|
||||
private void ShowCurrentStep()
|
||||
{
|
||||
if (CurrentStepIndex < 0 || CurrentStepIndex >= Steps.Count)
|
||||
{
|
||||
Debug.WriteLine($"[UserGuide] Invalid step index: {CurrentStepIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
var step = Steps[CurrentStepIndex];
|
||||
|
||||
// 验证目标控件是否可用
|
||||
if (step.TargetControl == null)
|
||||
{
|
||||
Debug.WriteLine($"[UserGuide] Step '{step.Title}' has no target control, showing full-screen overlay.");
|
||||
// 显示全屏提示
|
||||
ShowFullScreenGuide(step);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查目标控件是否在可视化树中
|
||||
if (!step.TargetControl.IsVisible || !IsControlInVisualTree(step.TargetControl))
|
||||
{
|
||||
Debug.WriteLine($"[UserGuide] Target control for step '{step.Title}' is not visible or not in visual tree. Skipping step.");
|
||||
|
||||
// 自动跳到下一步
|
||||
if (CurrentStepIndex < Steps.Count - 1)
|
||||
{
|
||||
CurrentStepIndex++;
|
||||
ShowCurrentStep();
|
||||
}
|
||||
else
|
||||
{
|
||||
Complete();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常显示引导
|
||||
ShowGuideForControl(step);
|
||||
}
|
||||
|
||||
private bool IsControlInVisualTree(Control control)
|
||||
{
|
||||
try
|
||||
{
|
||||
return control.GetVisualRoot() != null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Logging Strategy
|
||||
|
||||
**使用 Debug.WriteLine 进行调试日志:**
|
||||
|
||||
```csharp
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Penguin.AvaloniaUI.Utils;
|
||||
|
||||
internal static class Logger
|
||||
{
|
||||
[Conditional("DEBUG")]
|
||||
public static void Debug(string message)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[Penguin.AvaloniaUI] {message}");
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
public static void Warning(string message)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[WARNING] {message}");
|
||||
}
|
||||
|
||||
public static void Error(string message, Exception? ex = null)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[ERROR] {message}");
|
||||
if (ex != null)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Exception: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```csharp
|
||||
private void RefreshProperties(object? obj)
|
||||
{
|
||||
Logger.Debug($"RefreshProperties called with object type: {obj?.GetType().Name ?? "null"}");
|
||||
|
||||
try
|
||||
{
|
||||
// 核心逻辑
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Failed to refresh properties", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Error Messages Guidelines
|
||||
|
||||
**好的错误消息示例:**
|
||||
|
||||
```csharp
|
||||
// ❌ 不好的错误消息
|
||||
throw new Exception("Error");
|
||||
throw new ArgumentException("Invalid parameter");
|
||||
|
||||
// ✅ 好的错误消息
|
||||
throw new ArgumentNullException(nameof(selectedObject),
|
||||
"SelectedObject cannot be null. Please provide a valid object instance.");
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"Cannot apply theme: Application.Current is null. Ensure ThemeManager is called after App initialization.");
|
||||
|
||||
throw new NotSupportedException(
|
||||
$"Property type '{propertyType.Name}' is not supported by PropertyGrid. " +
|
||||
$"Supported types: string, int, double, bool, enum, DateTime.");
|
||||
```
|
||||
|
||||
**错误消息模板:**
|
||||
|
||||
```
|
||||
{What happened}: {Why it happened}. {How to fix it}.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Exception Types
|
||||
|
||||
**使用合适的异常类型:**
|
||||
|
||||
| 场景 | 异常类型 | 示例 |
|
||||
|------|---------|------|
|
||||
| 参数为 null | `ArgumentNullException` | `throw new ArgumentNullException(nameof(obj))` |
|
||||
| 参数值无效 | `ArgumentException` | `throw new ArgumentException("Invalid theme type", nameof(type))` |
|
||||
| 参数超出范围 | `ArgumentOutOfRangeException` | `throw new ArgumentOutOfRangeException(nameof(index))` |
|
||||
| 操作在当前状态无效 | `InvalidOperationException` | `throw new InvalidOperationException("Theme resources not loaded")` |
|
||||
| 功能未实现 | `NotImplementedException` | `throw new NotImplementedException("Custom editors not supported in MVP")` |
|
||||
| 功能不支持 | `NotSupportedException` | `throw new NotSupportedException($"Type {type} not supported")` |
|
||||
|
||||
---
|
||||
|
||||
### Try-Catch Guidelines
|
||||
|
||||
**何时使用 try-catch:**
|
||||
|
||||
```csharp
|
||||
// ✅ 捕获特定异常并提供上下文
|
||||
try
|
||||
{
|
||||
var value = property.GetValue(obj);
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Property '{property.Name}' getter threw an exception.",
|
||||
ex.InnerException ?? ex);
|
||||
}
|
||||
|
||||
// ✅ 记录异常并继续处理
|
||||
try
|
||||
{
|
||||
LoadThemeResource(uri);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"Failed to load theme resource: {uri}", ex);
|
||||
// 使用默认主题
|
||||
LoadDefaultTheme();
|
||||
}
|
||||
|
||||
// ❌ 不要捕获所有异常并吞没
|
||||
try
|
||||
{
|
||||
DoSomething();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 什么都不做 - 这是错误的!
|
||||
}
|
||||
|
||||
// ❌ 不要捕获异常后重新抛出同一个异常
|
||||
try
|
||||
{
|
||||
DoSomething();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw ex; // 错误:会丢失调用栈
|
||||
}
|
||||
|
||||
// ✅ 如果需要记录后重新抛出,使用 throw;
|
||||
try
|
||||
{
|
||||
DoSomething();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Operation failed", ex);
|
||||
throw; // 正确:保留调用栈
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Validation Helpers
|
||||
|
||||
**创建参数验证辅助方法:**
|
||||
|
||||
```csharp
|
||||
namespace Penguin.AvaloniaUI.Utils;
|
||||
|
||||
internal static class Guard
|
||||
{
|
||||
public static void NotNull<T>(T value, string paramName) where T : class
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(paramName);
|
||||
}
|
||||
}
|
||||
|
||||
public static void NotNullOrEmpty(string value, string paramName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
throw new ArgumentException("Value cannot be null or empty.", paramName);
|
||||
}
|
||||
}
|
||||
|
||||
public static void InRange(int value, int min, int max, string paramName)
|
||||
{
|
||||
if (value < min || value > max)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(paramName,
|
||||
$"Value must be between {min} and {max}, but was {value}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
public void SetProperty(string name, object value)
|
||||
{
|
||||
Guard.NotNullOrEmpty(name, nameof(name));
|
||||
Guard.NotNull(value, nameof(value));
|
||||
|
||||
// 核心逻辑
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user