Nuclex.Support/Source/PropertyChangedEventArgsHelper.cs
2024-06-13 18:36:21 +02:00

255 lines
9.5 KiB
C#

#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
#if NO_CONCURRENT_COLLECTIONS
using System.Collections.Generic;
#else
using System.Collections.Concurrent;
#endif
using System.ComponentModel;
#if !NO_LINQ_EXPRESSIONS
using System.Linq.Expressions;
#endif
namespace Nuclex.Support {
/// <summary>Contains helper methods for property change notifications</summary>
public static class PropertyChangedEventArgsHelper {
/// <summary>
/// A property change event argument container that indicates that all
/// properties have changed their value.
/// </summary>
public static readonly PropertyChangedEventArgs Wildcard =
new PropertyChangedEventArgs(null);
/// <summary>Initializes a new property changed argument helper</summary>
static PropertyChangedEventArgsHelper() {
#if NO_CONCURRENT_COLLECTIONS
cache = new Dictionary<string, PropertyChangedEventArgs>();
#else
cache = new ConcurrentDictionary<string, PropertyChangedEventArgs>();
#endif
}
#if !NO_LINQ_EXPRESSIONS
/// <summary>
/// Provides a property change argument container for the specified property
/// </summary>
/// <param name="property">
/// Property for which an event argument container will be provided
/// </param>
/// <returns>The event argument container for a property of the specified name</returns>
/// <remarks>
/// <para>
/// This method transparently caches instances of the argument containers
/// to avoid feeding the garbage collector. A typical application only has
/// in the order of tens to hundreds of different properties for which changes
/// will be reported, making a cache to avoid garbage collections viable.
/// </para>
/// <example>
/// <code>
/// PropertyChangedEventArgs arguments =
/// PropertyChangedEventArgsHelper.GetArgumentsFor(() => SomeProperty);
/// </code>
/// </example>
/// </remarks>
[Obsolete("Prefer the C# 'nameof()' operator to using a Linq expression")]
public static PropertyChangedEventArgs GetArgumentsFor<TValue>(
Expression<Func<TValue>> property
) {
return GetArgumentsFor(ObservableHelper.GetPropertyName(property));
}
#endif
/// <summary>
/// Provides a property change argument container for the specified property
/// </summary>
/// <param name="propertyName">
/// Property for which an event argument container will be provided
/// </param>
/// <returns>The event argument container for a property of the specified name</returns>
/// <remarks>
/// <para>
/// This method transparently caches instances of the argument containers
/// to avoid feeding the garbage collector. A typical application only has
/// in the order of tens to hundreds of different properties for which changes
/// will be reported, making a cache to avoid garbage collections viable.
/// </para>
/// <example>
/// <code>
/// PropertyChangedEventArgs arguments =
/// PropertyChangedEventArgsHelper.GetArgumentsFor("SomeProperty");
/// </code>
/// </example>
/// </remarks>
public static PropertyChangedEventArgs GetArgumentsFor(string propertyName) {
if(string.IsNullOrEmpty(propertyName)) {
return Wildcard;
}
#if NO_CONCURRENT_COLLECTIONS
lock(cache) {
// Try to reuse the change notification if an instance already exists
PropertyChangedEventArgs arguments;
if(!cache.TryGetValue(propertyName, out arguments)) {
arguments = new PropertyChangedEventArgs(propertyName);
cache.Add(propertyName, arguments);
}
return arguments;
}
#else
// If an instance for this property already exists, just return it
PropertyChangedEventArgs arguments;
if(cache.TryGetValue(propertyName, out arguments)) {
return arguments;
}
// No instance existed (at least a short moment ago), so create a new one
return cache.GetOrAdd(propertyName, new PropertyChangedEventArgs(propertyName));
#endif
}
#if !NO_LINQ_EXPRESSIONS
/// <summary>
/// Determines whether the property change affects the specified property
/// </summary>
/// <typeparam name="TValue">
/// Type of the property that will be tested for being affected
/// </typeparam>
/// <param name="arguments">
/// Property change that has been reported by the observed object
/// </param>
/// <param name="property">Property that will be tested for being affected</param>
/// <returns>Whether the specified property is affected by the property change</returns>
/// <remarks>
/// <para>
/// By using this method, you can shorten the code needed to test whether
/// a property change notification affects a specific property. You also
/// avoid hardcoding the property name, which would have the adverse effect
/// of not updating the textual property names during F2 refactoring.
/// </para>
/// <example>
/// <code>
/// private void propertyChanged(object sender, PropertyChangedEventArgs arguments) {
/// if(arguments.AreAffecting(() => ViewModel.DisplayedValue)) {
/// updateDisplayedValueFromViewModel();
/// } // Do not use else if here or wildcards will not work
/// if(arguments.AreAffecting(() => ViewModel.OtherValue)) {
/// updateOtherValueFromViewModel();
/// }
/// }
/// </code>
/// </example>
/// </remarks>
[Obsolete("Prefer the C# 'nameof()' operator to using a Linq expression")]
public static bool AreAffecting<TValue>(
this PropertyChangedEventArgs arguments, Expression<Func<TValue>> property
) {
if(arguments.AffectAllProperties()) {
return true;
}
string propertyName = ObservableHelper.GetPropertyName(property);
return (arguments.PropertyName == propertyName);
}
#endif
/// <summary>
/// Determines whether the property change affects the specified property
/// </summary>
/// <param name="arguments">
/// Property change that has been reported by the observed object
/// </param>
/// <param name="propertyName">Property that will be tested for being affected</param>
/// <returns>Whether the specified property is affected by the property change</returns>
/// <remarks>
/// <para>
/// By using this method, you can shorten the code needed to test whether
/// a property change notification affects a specific property.
/// </para>
/// <example>
/// <code>
/// private void propertyChanged(object sender, PropertyChangedEventArgs arguments) {
/// if(arguments.AreAffecting("DisplayedValue")) {
/// updateDisplayedValueFromViewModel();
/// } // Do not use else if here or wildcards will not work
/// if(arguments.AreAffecting("OtherValue")) {
/// updateOtherValueFromViewModel();
/// }
/// }
/// </code>
/// </example>
/// </remarks>
public static bool AreAffecting(
this PropertyChangedEventArgs arguments, string propertyName
) {
if(arguments.AffectAllProperties()) {
return true;
}
return (arguments.PropertyName == propertyName);
}
/// <summary>Determines whether a property change notification is a wildcard</summary>
/// <param name="arguments">
/// Property change notification that will be checked on being a wildcard
/// </param>
/// <returns>
/// Whether the property change is a wildcard, indicating that all properties
/// have changed.
/// </returns>
/// <remarks>
/// <para>
/// As stated on MSDN: "The PropertyChanged event can indicate all properties
/// on the object have changed by using either Nothing or String.Empty as
/// the property name in the PropertyChangedEventArgs."
/// </para>
/// <para>
/// This method offers an expressive way of checking for that eventuality.
/// </para>
/// <example>
/// <code>
/// private void propertyChanged(object sender, PropertyChangedEventArgs arguments) {
/// if(arguments.AffectAllProperties()) {
/// // Do something
/// }
/// }
/// </code>
/// </example>
/// </remarks>
public static bool AffectAllProperties(this PropertyChangedEventArgs arguments) {
return string.IsNullOrEmpty(arguments.PropertyName);
}
/// <summary>
/// Caches PropertyChangedEventArgs instances to avoid feeding the garbage collector
/// </summary>
#if NO_CONCURRENT_COLLECTIONS
private static readonly Dictionary<string, PropertyChangedEventArgs> cache;
#else
private static readonly ConcurrentDictionary<string, PropertyChangedEventArgs> cache;
#endif
}
} // namespace Nuclex.Support