port from perforce
This commit is contained in:
26
intromat/NodeNetworkToolkit/ValueNode/ValidationAction.cs
Normal file
26
intromat/NodeNetworkToolkit/ValueNode/ValidationAction.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace NodeNetwork.Toolkit.ValueNode
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Action that should be taken based on the validation result
|
||||
/// </summary>
|
||||
public enum ValidationAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Don't run the validation. (LatestValidation is not updated)
|
||||
/// </summary>
|
||||
DontValidate,
|
||||
/// <summary>
|
||||
/// Run the validation, but ignore the result and assume the network is valid.
|
||||
/// </summary>
|
||||
IgnoreValidation,
|
||||
/// <summary>
|
||||
/// Run the validation and if the network is invalid then wait until it is valid.
|
||||
/// </summary>
|
||||
WaitForValid,
|
||||
/// <summary>
|
||||
/// Run the validation and if the network is invalid then make default(T) the current value.
|
||||
/// </summary>
|
||||
PushDefaultValue
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using NodeNetwork.ViewModels;
|
||||
using NodeNetwork.Views;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace NodeNetwork.Toolkit.ValueNode
|
||||
{
|
||||
/// <summary>
|
||||
/// An editor for ValueNodeInputViewModel or ValueNodeOutputViewModel.
|
||||
/// For inputs, this class can provide values when no connection is present.
|
||||
/// For outputs, this class can provide a way to configure the value produced by the output.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class ValueEditorViewModel<T> : NodeEndpointEditorViewModel
|
||||
{
|
||||
static ValueEditorViewModel()
|
||||
{
|
||||
NNViewRegistrar.AddRegistration(() => new NodeEndpointEditorView(), typeof(IViewFor<ValueEditorViewModel<T>>));
|
||||
}
|
||||
|
||||
#region Value
|
||||
/// <summary>
|
||||
/// The value currently set in the editor.
|
||||
/// </summary>
|
||||
public T Value
|
||||
{
|
||||
get => _value;
|
||||
set => this.RaiseAndSetIfChanged(ref _value, value);
|
||||
}
|
||||
private T _value;
|
||||
#endregion
|
||||
|
||||
#region ValueChanged
|
||||
/// <summary>
|
||||
/// Observable that produces an object when the value changes.
|
||||
/// </summary>
|
||||
public IObservable<T> ValueChanged { get; }
|
||||
#endregion
|
||||
|
||||
public ValueEditorViewModel()
|
||||
{
|
||||
ValueChanged = this.WhenAnyValue(vm => vm.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DynamicData;
|
||||
using DynamicData.Alias;
|
||||
using DynamicData.Kernel;
|
||||
using NodeNetwork.ViewModels;
|
||||
using NodeNetwork.Views;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace NodeNetwork.Toolkit.ValueNode
|
||||
{
|
||||
/// <summary>
|
||||
/// A node input that keeps a list of the latest values produced by all of the connected ValueNodeOutputViewModels.
|
||||
/// This input can take multiple connections, ValueNodeInputViewModel cannot.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object this input can receive</typeparam>
|
||||
public class ValueListNodeInputViewModel<T> : NodeInputViewModel
|
||||
{
|
||||
static ValueListNodeInputViewModel()
|
||||
{
|
||||
NNViewRegistrar.AddRegistration(() => new NodeInputView(), typeof(IViewFor<ValueListNodeInputViewModel<T>>));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current values of the outputs connected to this input
|
||||
/// </summary>
|
||||
public IObservableList<T> Values { get; }
|
||||
|
||||
public ValueListNodeInputViewModel()
|
||||
{
|
||||
MaxConnections = Int32.MaxValue;
|
||||
ConnectionValidator = pending => new ConnectionValidationResult(
|
||||
pending.Output is ValueNodeOutputViewModel<T> ||
|
||||
pending.Output is ValueNodeOutputViewModel<IObservableList<T>>,
|
||||
null
|
||||
);
|
||||
|
||||
var valuesFromSingles = Connections.Connect(c => c.Output is ValueNodeOutputViewModel<T>)
|
||||
.Transform(c => (ValueNodeOutputViewModel<T>)c.Output)
|
||||
//Note: this line used to be
|
||||
//.AutoRefresh(output => output.CurrentValue)
|
||||
//which ignored changes where CurrentValue didn't change.
|
||||
//This caused problems when the value object isn't replaced, but one of its properties changes.
|
||||
.AutoRefreshOnObservable(output => output.Value)
|
||||
// Null values are not allowed, so filter before transform
|
||||
.Filter(output => output.CurrentValue != null)
|
||||
.Transform(output => output.CurrentValue, true)
|
||||
// Any 'replace' changes that don't change the value should be refresh changes
|
||||
// This prevents issues where a value is updated, but it doesn't propagate through the network
|
||||
// because the connections didn't change.
|
||||
.Select(changes =>
|
||||
{
|
||||
if (changes.TotalChanges == changes.Replaced + changes.Refreshes)
|
||||
{
|
||||
bool allRefresh = true;
|
||||
var newChanges = new ChangeSet<T>();
|
||||
foreach (var change in changes)
|
||||
{
|
||||
if (change.Reason == ListChangeReason.Replace)
|
||||
{
|
||||
if (change.Type == ChangeType.Item)
|
||||
{
|
||||
if (change.Item.Previous != change.Item.Current)
|
||||
{
|
||||
allRefresh = false;
|
||||
break;
|
||||
}
|
||||
newChanges.Add(new Change<T>(ListChangeReason.Refresh, change.Item.Current, change.Item.Previous, change.Item.CurrentIndex, change.Item.PreviousIndex));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Does this ever occur?");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newChanges.Add(change);
|
||||
}
|
||||
}
|
||||
|
||||
if (allRefresh) return newChanges;
|
||||
}
|
||||
return changes;
|
||||
});
|
||||
|
||||
var valuesFromLists = Connections.Connect(c => c.Output is ValueNodeOutputViewModel<IObservableList<T>>)
|
||||
// Grab list of values from output, using switch to handle when the list object is replaced
|
||||
.Transform(c => ((ValueNodeOutputViewModel<IObservableList<T>>) c.Output).Value.Switch())
|
||||
// Materialize this changeset stream into a list (needed to make sure the next step is done dynamically)
|
||||
.AsObservableList()
|
||||
// Take the union of all values from all lists. This is done dynamically, so adding/removing new lists works as expected.
|
||||
.Or();
|
||||
|
||||
Values = valuesFromSingles.Or(valuesFromLists).AsObservableList();
|
||||
}
|
||||
}
|
||||
}
|
||||
224
intromat/NodeNetworkToolkit/ValueNode/ValueNodeInputViewModel.cs
Normal file
224
intromat/NodeNetworkToolkit/ValueNode/ValueNodeInputViewModel.cs
Normal file
@@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Reactive.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using NodeNetwork.ViewModels;
|
||||
using NodeNetwork.Views;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace NodeNetwork.Toolkit.ValueNode
|
||||
{
|
||||
/// <summary>
|
||||
/// A node input that keeps track of the latest value produced by either the connected ValueNodeOutputViewModel,
|
||||
/// or the ValueEditorViewModel in the Editor property.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object this input can receive</typeparam>
|
||||
public class ValueNodeInputViewModel<T> : ValueNodeInputViewModelBase
|
||||
{
|
||||
static ValueNodeInputViewModel()
|
||||
{
|
||||
NNViewRegistrar.AddRegistration(() => new NodeInputView(), typeof(IViewFor<ValueNodeInputViewModel<T>>));
|
||||
}
|
||||
|
||||
#region Value
|
||||
/// <summary>
|
||||
/// The value currently associated with this input.
|
||||
/// If the input is not connected, the value is taken from ValueEditorViewModel.Value in the Editor property.
|
||||
/// If the input is connected, the value is taken from ValueNodeOutputViewModel.LatestValue unless the network is not traversable.
|
||||
/// Note that this value may be equal to default(T) if there is an error somewhere.
|
||||
/// </summary>
|
||||
public T Value => _value.Value;
|
||||
private readonly ObservableAsPropertyHelper<T> _value;
|
||||
#endregion
|
||||
|
||||
#region ValueChanged
|
||||
/// <summary>
|
||||
/// An observable that fires when the input value changes.
|
||||
/// This may be because of a connection change, editor value change, network validation change, ...
|
||||
/// </summary>
|
||||
public IObservable<T> ValueChanged { get; }
|
||||
public override IObservable<Unit> UnitValueChanged { get; }
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new ValueNodeInputViewModel with the specified ValidationActions.
|
||||
/// The default values are carefully chosen and should probably not be changed unless you know what you are doing.
|
||||
/// </summary>
|
||||
/// <param name="connectionChangedValidationAction">The validation behaviour when the connection of this input changes.</param>
|
||||
/// <param name="connectedValueChangedValidationAction">The validation behaviour when the value of this input changes.</param>
|
||||
public ValueNodeInputViewModel(
|
||||
ValidationAction connectionChangedValidationAction = ValidationAction.PushDefaultValue,
|
||||
ValidationAction connectedValueChangedValidationAction = ValidationAction.IgnoreValidation
|
||||
)
|
||||
{
|
||||
MaxConnections = 1;
|
||||
ConnectionValidator = pending => new ConnectionValidationResult(pending.Output is ValueNodeOutputViewModel<T>, null);
|
||||
|
||||
var connectedValues = GenerateConnectedValuesBinding(connectionChangedValidationAction, connectedValueChangedValidationAction);
|
||||
|
||||
var localValues = this.WhenAnyValue(vm => vm.Editor)
|
||||
.Select(e =>
|
||||
{
|
||||
if (e == null)
|
||||
{
|
||||
return Observable.Return(default(T));
|
||||
}
|
||||
else if (!(e is ValueEditorViewModel<T>))
|
||||
{
|
||||
throw new Exception($"The endpoint editor is not a subclass of ValueEditorViewModel<{typeof(T).Name}>");
|
||||
}
|
||||
else
|
||||
{
|
||||
return ((ValueEditorViewModel<T>)e).ValueChanged;
|
||||
}
|
||||
})
|
||||
.Switch();
|
||||
|
||||
var valueChanged = Observable.CombineLatest(connectedValues, localValues,
|
||||
(connectedValue, localValue) => Connections.Count == 0 ? localValue : connectedValue
|
||||
).Publish();
|
||||
valueChanged.Connect();
|
||||
valueChanged.ToProperty(this, vm => vm.Value, out _value);
|
||||
|
||||
ValueChanged = Observable
|
||||
.Defer(() => Observable.Return(Value))
|
||||
.Concat(valueChanged);
|
||||
|
||||
UnitValueChanged = ValueChanged
|
||||
.Select(_ => Unit.Default)
|
||||
.StartWith(Unit.Default);
|
||||
}
|
||||
|
||||
private IObservable<T> GenerateConnectedValuesBinding(ValidationAction connectionChangedValidationAction, ValidationAction connectedValueChangedValidationAction)
|
||||
{
|
||||
var onConnectionChanged = this.Connections.Connect().Select(_ => Unit.Default).StartWith(Unit.Default)
|
||||
.Select(_ => Connections.Count == 0 ? null : Connections.Items.First());
|
||||
|
||||
//On connection change
|
||||
IObservable<IObservable<T>> connectionObservables;
|
||||
if (connectionChangedValidationAction != ValidationAction.DontValidate)
|
||||
{
|
||||
//Either run network validation
|
||||
IObservable<NetworkValidationResult> postValidation = onConnectionChanged
|
||||
.SelectMany(con => Parent?.Parent?.UpdateValidation.Execute() ?? Observable.Return(new NetworkValidationResult(true, true, null)));
|
||||
|
||||
if (connectionChangedValidationAction == ValidationAction.WaitForValid)
|
||||
{
|
||||
//And wait until the validation is successful
|
||||
postValidation = postValidation.SelectMany(validation =>
|
||||
validation.NetworkIsTraversable
|
||||
? Observable.Return(validation)
|
||||
: Parent.Parent.Validation.FirstAsync(val => val.NetworkIsTraversable));
|
||||
}
|
||||
|
||||
if (connectionChangedValidationAction == ValidationAction.PushDefaultValue)
|
||||
{
|
||||
//Or push a single default(T) if the validation fails
|
||||
connectionObservables = postValidation.Select(validation =>
|
||||
{
|
||||
if (Connections.Count == 0)
|
||||
{
|
||||
return Observable.Return(default(T));
|
||||
}
|
||||
else if(validation.NetworkIsTraversable)
|
||||
{
|
||||
IObservable<T> connectedObservable =
|
||||
((ValueNodeOutputViewModel<T>) Connections.Items.First().Output).Value;
|
||||
if (connectedObservable == null)
|
||||
{
|
||||
throw new Exception($"The value observable for output '{Connections.Items.First().Output.Name}' is null.");
|
||||
}
|
||||
return connectedObservable;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Observable.Return(default(T));
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
//Grab the values observable from the connected output
|
||||
connectionObservables = postValidation
|
||||
.Select(_ =>
|
||||
{
|
||||
if (Connections.Count == 0)
|
||||
{
|
||||
return Observable.Return(default(T));
|
||||
}
|
||||
else
|
||||
{
|
||||
IObservable<T> connectedObservable =
|
||||
((ValueNodeOutputViewModel<T>)Connections.Items.First().Output).Value;
|
||||
if (connectedObservable == null)
|
||||
{
|
||||
throw new Exception($"The value observable for output '{Connections.Items.First().Output.Name}' is null.");
|
||||
}
|
||||
return connectedObservable;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Or just grab the values observable from the connected output
|
||||
connectionObservables = onConnectionChanged.Select(con =>
|
||||
{
|
||||
if (con == null)
|
||||
{
|
||||
return Observable.Return(default(T));
|
||||
}
|
||||
else
|
||||
{
|
||||
IObservable<T> connectedObservable =
|
||||
((ValueNodeOutputViewModel<T>)con.Output).Value;
|
||||
if (connectedObservable == null)
|
||||
{
|
||||
throw new Exception($"The value observable for output '{Connections.Items.First().Output.Name}' is null.");
|
||||
}
|
||||
return connectedObservable;
|
||||
}
|
||||
});
|
||||
}
|
||||
IObservable<T> connectedValues = connectionObservables.SelectMany(c => c);
|
||||
|
||||
//On connected output value change, either just push the value as is
|
||||
if (connectedValueChangedValidationAction != ValidationAction.DontValidate)
|
||||
{
|
||||
//Or run a network validation
|
||||
IObservable<NetworkValidationResult> postValidation = connectedValues.SelectMany(v =>
|
||||
Parent?.Parent?.UpdateValidation.Execute() ?? Observable.Return(new NetworkValidationResult(true, true, null)));
|
||||
if (connectedValueChangedValidationAction == ValidationAction.WaitForValid)
|
||||
{
|
||||
//And wait until the validation is successful
|
||||
postValidation = postValidation.SelectMany(validation =>
|
||||
validation.IsValid
|
||||
? Observable.Return(validation)
|
||||
: Parent.Parent.Validation.FirstAsync(val => val.IsValid));
|
||||
}
|
||||
|
||||
connectedValues = postValidation.Select(validation =>
|
||||
{
|
||||
if (Connections.Count == 0
|
||||
|| connectionChangedValidationAction == ValidationAction.PushDefaultValue && !validation.NetworkIsTraversable
|
||||
|| connectedValueChangedValidationAction == ValidationAction.PushDefaultValue && !validation.IsValid)
|
||||
{
|
||||
//Push default(T) if the network isn't valid
|
||||
return default;
|
||||
}
|
||||
|
||||
//Or just ignore the validation and push the value as is
|
||||
return ((ValueNodeOutputViewModel<T>) this.Connections.Items.First().Output).CurrentValue;
|
||||
});
|
||||
}
|
||||
|
||||
return connectedValues;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Reactive;
|
||||
using NodeNetwork.ViewModels;
|
||||
|
||||
namespace NodeNetwork.Toolkit.ValueNode
|
||||
{
|
||||
public abstract class ValueNodeInputViewModelBase : NodeInputViewModel
|
||||
{
|
||||
|
||||
#region ValueChanged
|
||||
/// <summary>
|
||||
/// An observable that fires when the input value changes.
|
||||
/// This may be because of a connection change, editor value change, network validation change, ...
|
||||
/// </summary>
|
||||
public abstract IObservable<Unit> UnitValueChanged { get; }
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Reactive.Concurrency;
|
||||
using NodeNetwork.ViewModels;
|
||||
using NodeNetwork.Views;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace NodeNetwork.Toolkit.ValueNode
|
||||
{
|
||||
/// <summary>
|
||||
/// A viewmodel for a node output that produces a value based on the inputs.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object produced by this output.</typeparam>
|
||||
public class ValueNodeOutputViewModel<T> : NodeOutputViewModel
|
||||
{
|
||||
static ValueNodeOutputViewModel()
|
||||
{
|
||||
NNViewRegistrar.AddRegistration(() => new NodeOutputView(), typeof(IViewFor<ValueNodeOutputViewModel<T>>));
|
||||
}
|
||||
|
||||
#region Value
|
||||
/// <summary>
|
||||
/// Observable that produces the value every time it changes.
|
||||
/// </summary>
|
||||
public IObservable<T> Value
|
||||
{
|
||||
get => _value;
|
||||
set => this.RaiseAndSetIfChanged(ref _value, value);
|
||||
}
|
||||
private IObservable<T> _value;
|
||||
#endregion
|
||||
|
||||
#region CurrentValue
|
||||
/// <summary>
|
||||
/// The latest value produced by this output.
|
||||
/// </summary>
|
||||
public T CurrentValue => _currentValue.Value;
|
||||
private readonly ObservableAsPropertyHelper<T> _currentValue;
|
||||
#endregion
|
||||
|
||||
public ValueNodeOutputViewModel()
|
||||
{
|
||||
this.WhenAnyObservable(vm => vm.Value).ToProperty(this, vm => vm.CurrentValue, out _currentValue, false, Scheduler.Immediate);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user