diff --git a/src/MaaWpfGui/Utilities/PropertyDependsOnHelper.cs b/src/MaaWpfGui/Utilities/PropertyDependsOnHelper.cs new file mode 100644 index 0000000000..5ae7099ad5 --- /dev/null +++ b/src/MaaWpfGui/Utilities/PropertyDependsOnHelper.cs @@ -0,0 +1,153 @@ +// +// Part of the MaaWpfGui project, maintained by the MaaAssistantArknights team (Maa Team) +// Copyright (C) 2021-2025 MaaAssistantArknights Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License v3.0 only as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY +// + +#nullable enable +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace MaaWpfGui.Utilities; + +/// +/// 提供对 PropertyDependsOnAttribute 的支持:当源属性改变时,自动通知依赖的属性。 +/// 这是一个静态工具类,可以被 ViewModel 和 Screen 使用。 +/// +public static class PropertyDependsOnHelper +{ + // 存储每个实例的属性依赖关系:使用 ConditionalWeakTable 避免内存泄漏 + private static readonly ConditionalWeakTable>> _instanceDependencies = new(); + + // 存储每个实例的事件处理器,以便在需要时可以取消订阅 + private static readonly ConditionalWeakTable _instanceHandlers = new(); + + /// + /// 扫描指定实例的属性,查找 PropertyDependsOnAttribute 并设置通知。 + /// 从派生类型的构造函数中调用此方法。 + /// + /// 要实现属性依赖的实例(必须实现 INotifyPropertyChanged) + /// 当 instance 为 null 时抛出 + /// 当 instance 不实现 INotifyPropertyChanged 时抛出 + public static void InitializePropertyDependencies(object instance) + { + ArgumentNullException.ThrowIfNull(instance); + + if (instance is not INotifyPropertyChanged notifyPropertyChanged) + { + throw new ArgumentException("实例必须实现 INotifyPropertyChanged 接口", nameof(instance)); + } + + var type = instance.GetType(); + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); + + var propertyDependencies = new Dictionary>(); // key: source property name, value: dependent property names + + foreach (var property in properties) + { + var dependsOnAttributes = property.GetCustomAttributes(true); + foreach (var attribute in dependsOnAttributes.Where(i => i.PropertyNames.Length > 0)) + { + foreach (var key in attribute.PropertyNames) + { + if (!propertyDependencies.TryGetValue(key, out var value)) + { + value = []; + propertyDependencies[key] = value; + } + + if (propertyDependencies.TryGetValue(property.Name, out var values) && values.Contains(key)) + { + throw new ArgumentException($"属性 {key} 依赖于属性 {property.Name}, 但它们之间存在循环依赖关系"); + } + + value.Add(property.Name); + } + } + } + + if (propertyDependencies.Count > 0) + { + _instanceDependencies.Add(instance, propertyDependencies); + + void handler(object? sender, PropertyChangedEventArgs e) + { + if (sender != instance) + { + return; + } + + if (string.IsNullOrEmpty(e.PropertyName) || !propertyDependencies.TryGetValue(e.PropertyName, out var dependentProperties)) + { + return; + } + + foreach (var dependentProperty in dependentProperties) + { + NotifyPropertyChange(instance, dependentProperty); + } + } + + _instanceHandlers.Add(instance, handler); + notifyPropertyChanged.PropertyChanged += handler; + } + } + + /// + /// 取消指定实例的属性依赖初始化,移除事件订阅。 + /// + /// 要取消初始化的实例 + public static void UninitializePropertyDependencies(object instance) + { + if (instance == null) + { + return; + } + + if (instance is INotifyPropertyChanged notifyPropertyChanged && _instanceHandlers.TryGetValue(instance, out var handler)) + { + notifyPropertyChanged.PropertyChanged -= handler; + _instanceHandlers.Remove(instance); + } + + _instanceDependencies.Remove(instance); + } + + /// + /// 通知属性改变。尝试使用 NotifyOfPropertyChange 方法(Stylet),如果不存在则直接触发 PropertyChanged 事件。 + /// + private static void NotifyPropertyChange(object instance, string propertyName) + { + // 首先尝试使用反射调用 NotifyOfPropertyChange 方法(Stylet 的 PropertyChangedBase 和 Screen 都有这个方法) + var type = instance.GetType(); + var method = type.GetMethod("NotifyOfPropertyChange", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(string) }, null); + + if (method != null) + { + method.Invoke(instance, new object[] { propertyName }); + return; + } + + // 如果没有 NotifyOfPropertyChange 方法,尝试直接触发 PropertyChanged 事件 + if (instance is INotifyPropertyChanged notifyPropertyChanged) + { + var eventField = type.GetField("PropertyChanged", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); + if (eventField?.GetValue(instance) is PropertyChangedEventHandler eventHandler) + { + eventHandler.Invoke(instance, new PropertyChangedEventArgs(propertyName)); + } + } + } +} + diff --git a/src/MaaWpfGui/ViewModels/LogCardViewModel.cs b/src/MaaWpfGui/ViewModels/LogCardViewModel.cs index 688896e499..45903b06ab 100644 --- a/src/MaaWpfGui/ViewModels/LogCardViewModel.cs +++ b/src/MaaWpfGui/ViewModels/LogCardViewModel.cs @@ -23,7 +23,7 @@ namespace MaaWpfGui.ViewModels /// /// Represents a grouped log card that contains several . /// - public class LogCardViewModel : PropertyDependsOnViewModel + public class LogCardViewModel : PropertyChangedBase { public ObservableCollection Items { get; } = new(); @@ -40,7 +40,7 @@ namespace MaaWpfGui.ViewModels public LogCardViewModel() { - InitializePropertyDependencies(); + PropertyDependsOnHelper.InitializePropertyDependencies(this); // Keep StartTime/EndTime in sync when Items changes or an item's Time updates. Items.CollectionChanged += Items_CollectionChanged; diff --git a/src/MaaWpfGui/ViewModels/LogItemViewModel.cs b/src/MaaWpfGui/ViewModels/LogItemViewModel.cs index 1b5645d4f0..7c017bf45b 100644 --- a/src/MaaWpfGui/ViewModels/LogItemViewModel.cs +++ b/src/MaaWpfGui/ViewModels/LogItemViewModel.cs @@ -25,7 +25,7 @@ namespace MaaWpfGui.ViewModels; /// /// The view model of log item. /// -public class LogItemViewModel : PropertyDependsOnViewModel +public class LogItemViewModel : PropertyChangedBase { /// /// Initializes a new instance of the class. @@ -38,7 +38,7 @@ public class LogItemViewModel : PropertyDependsOnViewModel /// The toolTip public LogItemViewModel(string content, string color = UiLogColor.Message, string weight = "Regular", string dateFormat = "", bool showTime = true, ToolTip? toolTip = null) { - InitializePropertyDependencies(); + PropertyDependsOnHelper.InitializePropertyDependencies(this); if (string.IsNullOrEmpty(dateFormat)) { diff --git a/src/MaaWpfGui/ViewModels/PropertyDependsOnViewModel.cs b/src/MaaWpfGui/ViewModels/PropertyDependsOnViewModel.cs deleted file mode 100644 index 13d9b977ae..0000000000 --- a/src/MaaWpfGui/ViewModels/PropertyDependsOnViewModel.cs +++ /dev/null @@ -1,81 +0,0 @@ -// -// Part of the MaaWpfGui project, maintained by the MaaAssistantArknights team (Maa Team) -// Copyright (C) 2021-2025 MaaAssistantArknights Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License v3.0 only as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY -// - -#nullable enable -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Reflection; -using MaaWpfGui.Utilities; -using Stylet; - -namespace MaaWpfGui.ViewModels -{ - /// - /// Provides support for PropertyDependsOnAttribute: when a source property changes, - /// dependent properties are automatically notified. - /// - public abstract class PropertyDependsOnViewModel : PropertyChangedBase - { - private readonly Dictionary> _propertyDependencies = []; // key: source property name, value: dependent property names - - /// - /// Scan for PropertyDependsOnAttribute on this instance's properties and wire notifications. - /// Call this from the derived type's constructor. - /// - protected void InitializePropertyDependencies() - { - var type = GetType(); - var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); - - foreach (var property in properties) - { - var dependsOnAttributes = property.GetCustomAttributes(true); - foreach (var attribute in dependsOnAttributes.Where(i => i.PropertyNames.Length > 0)) - { - foreach (var key in attribute.PropertyNames) - { - if (!_propertyDependencies.TryGetValue(key, out var value)) - { - value = []; - _propertyDependencies[key] = value; - } - - if (_propertyDependencies.TryGetValue(property.Name, out var values) && values.Contains(key)) - { - throw new ArgumentException($"属性 {key} 依赖于属性 {property.Name}, 但它们之间存在循环依赖关系"); - } - - value.Add(property.Name); - } - } - } - - PropertyChanged += OnPropertyChanged; - } - - private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (string.IsNullOrEmpty(e.PropertyName) || !_propertyDependencies.TryGetValue(e.PropertyName, out var value)) - { - return; - } - - foreach (var dependentProperty in value) - { - NotifyOfPropertyChange(dependentProperty); - } - } - } -} diff --git a/src/MaaWpfGui/ViewModels/TaskViewModel.cs b/src/MaaWpfGui/ViewModels/TaskViewModel.cs index 55b4af0f4c..70ac052173 100644 --- a/src/MaaWpfGui/ViewModels/TaskViewModel.cs +++ b/src/MaaWpfGui/ViewModels/TaskViewModel.cs @@ -29,11 +29,11 @@ using Stylet; namespace MaaWpfGui.ViewModels; -public abstract class TaskViewModel : PropertyDependsOnViewModel +public abstract class TaskViewModel : PropertyChangedBase { protected TaskViewModel() { - InitializePropertyDependencies(); + PropertyDependsOnHelper.InitializePropertyDependencies(this); } protected T? GetTaskConfig()