Files
ui/docs/architecture/error-handling.md
2025-10-16 15:08:42 +08:00

11 KiB
Raw Blame History

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 参数验证:

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 错误处理:

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 错误处理:

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 进行调试日志:

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}");
        }
    }
}

使用示例:

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

好的错误消息示例:

// ❌ 不好的错误消息
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

// ✅ 捕获特定异常并提供上下文
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

创建参数验证辅助方法:

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));

    // 核心逻辑
}