未知泛型类型时注册ObservableCollection的CollectionChanged事件方案
Alright, let's tackle this problem. You want a base class that tracks all property changes, uses a dictionary backed by CallerMemberName for storage, and specifically handles ObservableCollection<T> by hooking into their CollectionChanged events (and unhooking old values to avoid memory leaks). Here's a complete, robust implementation:
1. Base Class Core Setup
First, we'll define the base class with the backing dictionary, basic GetValue<T>/SetValue<T> methods, and infrastructure to handle both property and collection changes.
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; public class ObservableBase : INotifyPropertyChanged { // Backing store for all properties, keyed by property name private readonly Dictionary<string, object> _backingFields = new Dictionary<string, object>(); // Standard property changed event for UI binding or change tracking public event PropertyChangedEventHandler? PropertyChanged; // Maps collection instances to their parent property names (for triggering property updates on collection changes) private readonly Dictionary<object, string> _collectionPropertyMap = new Dictionary<object, string>(); // Handler for collection changes - customize this logic to your needs private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (sender != null && _collectionPropertyMap.TryGetValue(sender, out var propertyName)) { // Trigger a property change for the parent property (useful for UI updates) OnPropertyChanged(propertyName); // Add custom logic here (e.g., validation, logging) } }
2. Implement RegisterIfObservable<T>
This method will handle attaching/detaching CollectionChanged events for any collection that implements INotifyCollectionChanged (which ObservableCollection<T> does). We'll build it to handle both new collection registration and old collection cleanup to prevent memory leaks.
/// <summary> /// Registers collection change events for observable collections, and cleans up old collection subscriptions /// </summary> /// <param name="newValue">New collection to observe</param> /// <param name="oldValue">Old collection to stop observing (optional)</param> /// <param name="propertyName">Name of the parent property (auto-populated via CallerMemberName)</param> public void RegisterIfObservable<T>(T newValue, T? oldValue = default, [CallerMemberName] string propertyName = "") { // Unhook old collection first to avoid memory leaks if (oldValue is INotifyCollectionChanged oldObservable) { oldObservable.CollectionChanged -= OnCollectionChanged; _collectionPropertyMap.Remove(oldObservable); } // Hook new collection if it's observable if (newValue is INotifyCollectionChanged newObservable) { newObservable.CollectionChanged += OnCollectionChanged; _collectionPropertyMap[newObservable] = propertyName; } }
3. Integrate with SetValue<T>
Update the SetValue<T> method to leverage RegisterIfObservable whenever a property is updated, passing both old and new values to handle event cleanup and registration automatically.
/// <summary> /// Retrieves a property value from the backing dictionary /// </summary> protected T GetValue<T>([CallerMemberName] string propertyName = "") { _backingFields.TryGetValue(propertyName, out var value); return value is T typedValue ? typedValue : default!; } /// <summary> /// Updates a property value, triggers property changed, and manages collection event subscriptions /// </summary> protected bool SetValue<T>(T newValue, [CallerMemberName] string propertyName = "") { var oldValue = GetValue<T>(propertyName); // Do nothing if values are equal if (EqualityComparer<T>.Default.Equals(oldValue, newValue)) { return false; } // Update backing store _backingFields[propertyName] = newValue; // Handle collection event registration/unregistration RegisterIfObservable(newValue, oldValue, propertyName); // Trigger property changed notification OnPropertyChanged(propertyName); return true; } protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
4. Usage Example
Here's how you'd use this base class in a view model or data class:
public class TodoListViewModel : ObservableBase { public ObservableCollection<TodoItem> TodoItems { get => GetValue<ObservableCollection<TodoItem>>(); set => SetValue(value); } public string ListName { get => GetValue<string>(); set => SetValue(value); } public TodoListViewModel() { TodoItems = new ObservableCollection<TodoItem>(); ListName = "My Tasks"; } } public class TodoItem { public string Title { get; set; } = string.Empty; public bool IsCompleted { get; set; } }
Key Notes
- Memory Leak Prevention: We always unhook the old collection's event before hooking the new one, and remove old collections from our mapping dictionary. This ensures unused collections can be garbage collected properly.
- Flexibility: By targeting
INotifyCollectionChangedinstead of justObservableCollection<T>, this works with any custom collection that implements the interface. - UI-Friendly: The
OnCollectionChangedhandler triggers a property change for the parent property, which ensures UI bindings update when the collection's contents change (e.g., items added/removed).
内容的提问来源于stack exchange,提问作者J4N

