A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1010 lines
34 KiB

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Data;
using Avalonia.Diagnostics;
using Avalonia.Styling;
using Avalonia.Utilities;
using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot;
namespace Avalonia.PropertyStore
{
internal class ValueStore
{
private readonly List<ValueFrame> _frames = new();
private Dictionary<int, IDisposable>? _localValueBindings;
private AvaloniaPropertyDictionary<EffectiveValue> _effectiveValues;
private int _inheritedValueCount;
private int _isEvaluating;
private int _frameGeneration;
private int _styling;
public ValueStore(AvaloniaObject owner) => Owner = owner;
public AvaloniaObject Owner { get; }
public ValueStore? InheritanceAncestor { get; private set; }
public bool IsEvaluating => _isEvaluating > 0;
public IReadOnlyList<ValueFrame> Frames => _frames;
public void BeginStyling() => ++_styling;
public void EndStyling()
{
if (--_styling == 0)
ReevaluateEffectiveValues();
}
public void AddFrame(ValueFrame style)
{
InsertFrame(style);
ReevaluateEffectiveValues();
}
public IDisposable AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source,
BindingPriority priority)
{
if (priority == BindingPriority.LocalValue)
{
var observer = new LocalValueBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
else
{
var effective = GetEffectiveValue(property);
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
var result = frame.AddBinding(property, source);
if (effective is null || priority <= effective.Priority)
result.Start();
return result;
}
}
public IDisposable AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<T> source,
BindingPriority priority)
{
if (priority == BindingPriority.LocalValue)
{
var observer = new LocalValueBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
else
{
var effective = GetEffectiveValue(property);
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
var result = frame.AddBinding(property, source);
if (effective is null || priority <= effective.Priority)
result.Start();
return result;
}
}
public IDisposable AddBinding<T>(
StyledPropertyBase<T> property,
IObservable<object?> source,
BindingPriority priority)
{
if (priority == BindingPriority.LocalValue)
{
var observer = new LocalValueUntypedBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
else
{
var effective = GetEffectiveValue(property);
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
var result = frame.AddBinding(property, source);
if (effective is null || priority <= effective.Priority)
result.Start();
return result;
}
}
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, IObservable<BindingValue<T>> source)
{
var observer = new DirectBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, IObservable<T> source)
{
var observer = new DirectBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
public IDisposable AddBinding<T>(DirectPropertyBase<T> property, IObservable<object?> source)
{
var observer = new DirectUntypedBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
observer.Start(source);
return observer;
}
public void ClearLocalValue(AvaloniaProperty property)
{
if (TryGetEffectiveValue(property, out var effective) &&
effective.Priority == BindingPriority.LocalValue)
{
ReevaluateEffectiveValue(property, effective, ignoreLocalValue: true);
}
}
public IDisposable? SetValue<T>(StyledPropertyBase<T> property, T value, BindingPriority priority)
{
if (property.ValidateValue?.Invoke(value) == false)
{
throw new ArgumentException($"{value} is not a valid value for '{property.Name}.");
}
if (priority != BindingPriority.LocalValue)
{
var frame = GetOrCreateImmediateValueFrame(property, priority, out _);
var result = frame.AddValue(property, value);
if (TryGetEffectiveValue(property, out var existing))
{
var effective = (EffectiveValue<T>)existing;
effective.SetAndRaise(this, result, priority);
}
else
{
var effectiveValue = new EffectiveValue<T>(Owner, property);
AddEffectiveValue(property, effectiveValue);
effectiveValue.SetAndRaise(this, result, priority);
}
return result;
}
else
{
if (TryGetEffectiveValue(property, out var existing))
{
var effective = (EffectiveValue<T>)existing;
effective.SetLocalValueAndRaise(this, property, value);
}
else
{
var effectiveValue = new EffectiveValue<T>(Owner, property);
AddEffectiveValue(property, effectiveValue);
effectiveValue.SetLocalValueAndRaise(this, property, value);
}
return null;
}
}
public object? GetValue(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var v))
return v.Value;
if (property.Inherits && TryGetInheritedValue(property, out v))
return v.Value;
return GetDefaultValue(property);
}
public T GetValue<T>(StyledPropertyBase<T> property)
{
if (_effectiveValues.TryGetValue(property, out var v))
return ((EffectiveValue<T>)v).Value;
if (property.Inherits && TryGetInheritedValue(property, out v))
return ((EffectiveValue<T>)v).Value;
return property.GetDefaultValue(Owner.GetType());
}
public bool IsAnimating(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var v))
return v.Priority <= BindingPriority.Animation;
return false;
}
public bool IsSet(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var v))
return v.Priority < BindingPriority.Inherited;
return false;
}
public void CoerceValue(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var v))
v.CoerceValue(this, property);
}
public Optional<T> GetBaseValue<T>(StyledPropertyBase<T> property)
{
if (TryGetEffectiveValue(property, out var v) &&
((EffectiveValue<T>)v).TryGetBaseValue(out var baseValue))
{
return baseValue;
}
return default;
}
public bool TryGetInheritedValue(
AvaloniaProperty property,
[NotNullWhen(true)] out EffectiveValue? result)
{
Debug.Assert(property.Inherits);
var i = InheritanceAncestor;
while (i is not null)
{
if (i.TryGetEffectiveValue(property, out result))
return true;
i = i.InheritanceAncestor;
}
result = null;
return false;
}
public void SetInheritanceParent(AvaloniaObject? newParent)
{
var values = AvaloniaPropertyDictionaryPool<OldNewValue>.Get();
var oldAncestor = InheritanceAncestor;
var newAncestor = newParent?.GetValueStore();
if (newAncestor?._inheritedValueCount == 0)
newAncestor = newAncestor.InheritanceAncestor;
// The old and new inheritance ancestors are the same, nothing to do here.
if (oldAncestor == newAncestor)
return;
// First get the old values from the old inheritance ancestor.
var f = oldAncestor;
while (f is not null)
{
var count = f._effectiveValues.Count;
for (var i = 0; i < count; ++i)
{
f._effectiveValues.GetKeyValue(i, out var key, out var value);
if (key.Inherits)
values.TryAdd(key, new(value));
}
f = f.InheritanceAncestor;
}
f = newAncestor;
// Get the new values from the new inheritance ancestor.
while (f is not null)
{
var count = f._effectiveValues.Count;
for (var i = 0; i < count; ++i)
{
f._effectiveValues.GetKeyValue(i, out var key, out var value);
if (!key.Inherits)
continue;
if (values.TryGetValue(key, out var existing))
{
if (existing.NewValue is null)
values[key] = existing.WithNewValue(value);
}
else
{
values.Add(key, new(null, value));
}
}
f = f.InheritanceAncestor;
}
OnInheritanceAncestorChanged(newAncestor);
// Raise PropertyChanged events where necessary on this object and inheritance children.
{
var count = values.Count;
for (var i = 0; i < count; ++i)
{
values.GetKeyValue(i, out var key, out var v);
var oldValue = v.OldValue;
var newValue = v.NewValue;
if (oldValue != newValue)
InheritedValueChanged(key, oldValue, newValue);
}
}
AvaloniaPropertyDictionaryPool<OldNewValue>.Release(values);
}
/// <summary>
/// Called by non-LocalValue binding entries to re-evaluate the effective value when the
/// binding produces a new value.
/// </summary>
/// <param name="entry">The binding entry.</param>
/// <param name="priority">The priority of binding which produced a new value.</param>
public void OnBindingValueChanged(
IValueEntry entry,
BindingPriority priority)
{
Debug.Assert(priority != BindingPriority.LocalValue);
var property = entry.Property;
if (TryGetEffectiveValue(property, out var existing))
{
if (priority <= existing.BasePriority)
ReevaluateEffectiveValue(property, existing);
}
else
{
AddEffectiveValueAndRaise(property, entry, priority);
}
}
/// <summary>
/// Called by non-LocalValue binding entries to re-evaluate the effective value when the
/// binding produces an unset value.
/// </summary>
/// <param name="property">The bound property.</param>
/// <param name="priority">The priority of binding which produced a new value.</param>
public void OnBindingValueCleared(AvaloniaProperty property, BindingPriority priority)
{
Debug.Assert(priority != BindingPriority.LocalValue);
if (TryGetEffectiveValue(property, out var existing))
{
if (priority <= existing.Priority)
ReevaluateEffectiveValue(property, existing);
}
}
/// <summary>
/// Called by a <see cref="ValueFrame"/> when its <see cref="ValueFrame.IsActive"/>
/// state changes.
/// </summary>
/// <param name="frame">The frame which produced the change.</param>
public void OnFrameActivationChanged(ValueFrame frame)
{
if (frame.EntryCount == 0)
return;
else if (frame.EntryCount == 1)
{
var property = frame.GetEntry(0).Property;
_effectiveValues.TryGetValue(property, out var current);
ReevaluateEffectiveValue(property, current);
}
else
ReevaluateEffectiveValues();
}
/// <summary>
/// Called by the parent value store when its inheritance ancestor changes.
/// </summary>
/// <param name="ancestor">The new inheritance ancestor.</param>
public void OnInheritanceAncestorChanged(ValueStore? ancestor)
{
if (ancestor != this)
{
InheritanceAncestor = ancestor;
if (_inheritedValueCount > 0)
return;
}
var children = Owner.GetInheritanceChildren();
if (children is null)
return;
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().OnInheritanceAncestorChanged(ancestor);
}
}
/// <summary>
/// Called by <see cref="EffectiveValue{T}"/> when an property with inheritance enabled
/// changes its value on this value store.
/// </summary>
/// <param name="property">The property whose value changed.</param>
/// <param name="oldValue">The old value of the property.</param>
/// <param name="value">The effective value instance.</param>
public void OnInheritedEffectiveValueChanged<T>(
StyledPropertyBase<T> property,
T oldValue,
EffectiveValue<T> value)
{
Debug.Assert(property.Inherits);
var children = Owner.GetInheritanceChildren();
if (children is null)
return;
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, value.Value);
}
}
/// <summary>
/// Called by <see cref="EffectiveValue{T}"/> when an property with inheritance enabled
/// is removed from the effective values.
/// </summary>
/// <param name="property">The property whose value changed.</param>
/// <param name="oldValue">The old value of the property.</param>
public void OnInheritedEffectiveValueDisposed<T>(StyledPropertyBase<T> property, T oldValue)
{
Debug.Assert(property.Inherits);
var children = Owner.GetInheritanceChildren();
if (children is not null)
{
var defaultValue = property.GetDefaultValue(Owner.GetType());
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, defaultValue);
}
}
}
/// <summary>
/// Called when a <see cref="LocalValueBindingObserver{T}"/> or
/// <see cref="DirectBindingObserver{T}"/> completes.
/// </summary>
/// <param name="property">The previously bound property.</param>
/// <param name="observer">The observer.</param>
public void OnLocalValueBindingCompleted(AvaloniaProperty property, IDisposable observer)
{
if (_localValueBindings is not null &&
_localValueBindings.TryGetValue(property.Id, out var existing))
{
if (existing == observer)
{
_localValueBindings?.Remove(property.Id);
ClearLocalValue(property);
}
}
}
/// <summary>
/// Called when an inherited property changes on the value store of the inheritance ancestor.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
/// <param name="property">The property.</param>
/// <param name="oldValue">The old value of the property.</param>
/// <param name="newValue">The new value of the property.</param>
public void OnAncestorInheritedValueChanged<T>(
StyledPropertyBase<T> property,
T oldValue,
T newValue)
{
Debug.Assert(property.Inherits);
// If the inherited value is set locally, propagation stops here.
if (_effectiveValues.ContainsKey(property))
return;
using var notifying = PropertyNotifying.Start(Owner, property);
Owner.RaisePropertyChanged(
property,
oldValue,
newValue,
BindingPriority.Inherited,
true);
var children = Owner.GetInheritanceChildren();
if (children is null)
return;
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, newValue);
}
}
/// <summary>
/// Called by a <see cref="ValueFrame"/> to re-evaluate the effective value when a value
/// is removed.
/// </summary>
/// <param name="frame">The frame on which the change occurred.</param>
/// <param name="property">The property whose value was removed.</param>
public void OnValueEntryRemoved(ValueFrame frame, AvaloniaProperty property)
{
if (frame.EntryCount == 0)
_frames.Remove(frame);
if (TryGetEffectiveValue(property, out var existing))
{
if (frame.Priority <= existing.Priority)
ReevaluateEffectiveValue(property, existing);
}
}
public bool RemoveFrame(ValueFrame frame)
{
if (_frames.Remove(frame))
{
frame.Dispose();
++_frameGeneration;
ReevaluateEffectiveValues();
}
return false;
}
public void RemoveFrames(FrameType type)
{
var removed = false;
for (var i = _frames.Count - 1; i >= 0; --i)
{
var frame = _frames[i];
if (frame is not ImmediateValueFrame && frame.FramePriority.IsType(type))
{
_frames.RemoveAt(i);
frame.Dispose();
removed = true;
}
}
if (removed)
{
++_frameGeneration;
ReevaluateEffectiveValues();
}
}
public void RemoveFrames(IReadOnlyList<IStyle> styles)
{
var removed = false;
for (var i = _frames.Count - 1; i >= 0; --i)
{
var frame = _frames[i];
if (frame is StyleInstance style && styles.Contains(style.Source))
{
_frames.RemoveAt(i);
frame.Dispose();
removed = true;
}
}
if (removed)
{
++_frameGeneration;
ReevaluateEffectiveValues();
}
}
public AvaloniaPropertyValue GetDiagnostic(AvaloniaProperty property)
{
object? value;
BindingPriority priority;
if (_effectiveValues.TryGetValue(property, out var v))
{
value = v.Value;
priority = v.Priority;
}
else if (property.Inherits && TryGetInheritedValue(property, out v))
{
value = v.Value;
priority = BindingPriority.Inherited;
}
else
{
value = GetDefaultValue(property);
priority = BindingPriority.Unset;
}
return new AvaloniaPropertyValue(
property,
value,
priority,
null);
}
private int InsertFrame(ValueFrame frame)
{
Debug.Assert(!_frames.Contains(frame));
var index = BinarySearchFrame(frame.FramePriority);
_frames.Insert(index, frame);
++_frameGeneration;
frame.SetOwner(this);
return index;
}
private ImmediateValueFrame GetOrCreateImmediateValueFrame(
AvaloniaProperty property,
BindingPriority priority,
out int frameIndex)
{
Debug.Assert(priority != BindingPriority.LocalValue);
var index = BinarySearchFrame(priority.ToFramePriority());
if (index > 0 && _frames[index - 1] is ImmediateValueFrame f &&
f.Priority == priority &&
!f.Contains(property))
{
frameIndex = index - 1;
return f;
}
var result = new ImmediateValueFrame(priority);
frameIndex = InsertFrame(result);
return result;
}
private void AddEffectiveValue(AvaloniaProperty property, EffectiveValue effectiveValue)
{
_effectiveValues.Add(property, effectiveValue);
if (property.Inherits && _inheritedValueCount++ == 0)
OnInheritanceAncestorChanged(this);
}
/// <summary>
/// Adds a new effective value, raises the initial <see cref="AvaloniaObject.PropertyChanged"/>
/// event and notifies inheritance children if necessary .
/// </summary>
/// <param name="property">The property.</param>
/// <param name="entry">The value entry.</param>
/// <param name="priority">The value priority.</param>
private void AddEffectiveValueAndRaise(AvaloniaProperty property, IValueEntry entry, BindingPriority priority)
{
Debug.Assert(priority < BindingPriority.Inherited);
var effectiveValue = property.CreateEffectiveValue(Owner);
AddEffectiveValue(property, effectiveValue);
effectiveValue.SetAndRaise(this, entry, priority);
}
private void RemoveEffectiveValue(AvaloniaProperty property, int index)
{
_effectiveValues.RemoveAt(index);
if (property.Inherits && --_inheritedValueCount == 0)
OnInheritanceAncestorChanged(InheritanceAncestor);
}
private bool RemoveEffectiveValue(AvaloniaProperty property)
{
if (_effectiveValues.Remove(property))
{
if (property.Inherits && --_inheritedValueCount == 0)
OnInheritanceAncestorChanged(InheritanceAncestor);
return true;
}
return false;
}
private void InheritedValueChanged(
AvaloniaProperty property,
EffectiveValue? oldValue,
EffectiveValue? newValue)
{
Debug.Assert(oldValue != newValue);
Debug.Assert(oldValue is not null || newValue is not null);
// If the value is set locally, propagaton ends here.
if (_effectiveValues.ContainsKey(property) == true)
return;
using var notifying = PropertyNotifying.Start(Owner, property);
// Raise PropertyChanged on this object if necessary.
(oldValue ?? newValue!).RaiseInheritedValueChanged(Owner, property, oldValue, newValue);
var children = Owner.GetInheritanceChildren();
if (children is null)
return;
var count = children.Count;
for (var i = 0; i < count; ++i)
{
children[i].GetValueStore().InheritedValueChanged(property, oldValue, newValue);
}
}
private void ReevaluateEffectiveValue(
AvaloniaProperty property,
EffectiveValue? current,
bool ignoreLocalValue = false)
{
++_isEvaluating;
try
{
restart:
// Don't reevaluate if a styling pass is in effect, reevaluation will be done when
// it has finished.
if (_styling > 0)
return;
var generation = _frameGeneration;
// Notify the existing effective value that reevaluation is starting.
current?.BeginReevaluation(ignoreLocalValue);
// Iterate the frames to get the effective value.
for (var i = _frames.Count - 1; i >= 0; --i)
{
var frame = _frames[i];
var priority = frame.Priority;
var foundEntry = frame.TryGetEntryIfActive(property, out var entry, out var activeChanged);
// If the active state of the frame has changed since the last read, and
// the frame holds multiple values then we need to re-evaluate the
// effective values of all properties.
if (activeChanged && frame.EntryCount > 1)
{
ReevaluateEffectiveValues();
return;
}
// We're interested in the value if:
// - There is no current effective value, or
// - The value's priority is higher than the current effective value's priority, or
// - The value is a non-animation value and its priority is higher than the current
// effective value's base priority
var isRelevantPriority = current is null ||
priority < current.Priority ||
(priority > BindingPriority.Animation && priority < current.BasePriority);
if (foundEntry && isRelevantPriority && entry!.HasValue)
{
if (current is not null)
{
current.SetAndRaise(this, entry, priority);
}
else
{
current = property.CreateEffectiveValue(Owner);
AddEffectiveValue(property, current);
current.SetAndRaise(this, entry, priority);
}
}
if (generation != _frameGeneration)
goto restart;
if (current?.Priority < BindingPriority.Unset &&
current?.BasePriority < BindingPriority.Unset)
break;
}
current?.EndReevaluation();
if (current?.Priority == BindingPriority.Unset)
{
if (current.BasePriority == BindingPriority.Unset)
{
RemoveEffectiveValue(property);
current.DisposeAndRaiseUnset(this, property);
}
else
{
current.RemoveAnimationAndRaise(this, property);
}
}
}
finally
{
--_isEvaluating;
}
}
private void ReevaluateEffectiveValues()
{
++_isEvaluating;
try
{
restart:
// Don't reevaluate if a styling pass is in effect, reevaluation will be done when
// it has finished.
if (_styling > 0)
return;
var generation = _frameGeneration;
var count = _effectiveValues.Count;
// Notify the existing effective values that reevaluation is starting.
for (var i = 0; i < count; ++i)
_effectiveValues[i].BeginReevaluation();
// Iterate the frames, setting and creating effective values.
for (var i = _frames.Count - 1; i >= 0; --i)
{
var frame = _frames[i];
if (!frame.IsActive)
continue;
var priority = frame.Priority;
count = frame.EntryCount;
for (var j = 0; j < count; ++j)
{
var entry = frame.GetEntry(j);
var property = entry.Property;
// Skip if we already have a value/base value for this property.
if (_effectiveValues.TryGetValue(property, out var effectiveValue) &&
effectiveValue.BasePriority < BindingPriority.Unset)
continue;
if (!entry.HasValue)
continue;
if (effectiveValue is not null)
{
effectiveValue.SetAndRaise(this, entry, priority);
}
else
{
var v = property.CreateEffectiveValue(Owner);
AddEffectiveValue(property, v);
v.SetAndRaise(this, entry, priority);
}
if (generation != _frameGeneration)
goto restart;
}
}
// Remove all effective values that are still unset.
for (var i = _effectiveValues.Count - 1; i >= 0; --i)
{
_effectiveValues.GetKeyValue(i, out var key, out var e);
e.EndReevaluation();
if (e.Priority == BindingPriority.Unset)
{
RemoveEffectiveValue(key, i);
e.DisposeAndRaiseUnset(this, key);
if (i > _effectiveValues.Count)
break;
}
}
}
finally
{
--_isEvaluating;
}
}
private bool TryGetEffectiveValue(
AvaloniaProperty property,
[NotNullWhen(true)] out EffectiveValue? value)
{
if (_effectiveValues.TryGetValue(property, out value))
return true;
value = null;
return false;
}
private EffectiveValue? GetEffectiveValue(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var value))
return value;
return null;
}
private object? GetDefaultValue(AvaloniaProperty property)
{
return ((IStyledPropertyAccessor)property).GetDefaultValue(Owner.GetType());
}
private void DisposeExistingLocalValueBinding(AvaloniaProperty property)
{
if (_localValueBindings is not null &&
_localValueBindings.TryGetValue(property.Id, out var existing))
{
existing.Dispose();
}
}
private int BinarySearchFrame(FramePriority priority)
{
var lo = 0;
var hi = _frames.Count - 1;
// Binary search insertion point.
while (lo <= hi)
{
var i = lo + ((hi - lo) >> 1);
var order = priority - _frames[i].FramePriority;
if (order <= 0)
{
lo = i + 1;
}
else
{
hi = i - 1;
}
}
return lo;
}
private readonly struct OldNewValue
{
public OldNewValue(EffectiveValue? oldValue)
{
OldValue = oldValue;
NewValue = null;
}
public OldNewValue(EffectiveValue? oldValue, EffectiveValue? newValue)
{
OldValue = oldValue;
NewValue = newValue;
}
public readonly EffectiveValue? OldValue;
public readonly EffectiveValue? NewValue;
public OldNewValue WithNewValue(EffectiveValue newValue) => new(OldValue, newValue);
}
}
}