csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
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.
333 lines
13 KiB
333 lines
13 KiB
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
|
|
using System;
|
|
using System.Reactive;
|
|
using System.Reactive.Disposables;
|
|
using System.Reactive.Linq;
|
|
using System.Reactive.Subjects;
|
|
using Avalonia.Data;
|
|
using Avalonia.Reactive;
|
|
|
|
namespace Avalonia
|
|
{
|
|
/// <summary>
|
|
/// Provides extension methods for <see cref="AvaloniaObject"/> and related classes.
|
|
/// </summary>
|
|
public static class AvaloniaObjectExtensions
|
|
{
|
|
/// <summary>
|
|
/// Converts an <see cref="IObservable{T}"/> to an <see cref="IBinding"/>.
|
|
/// </summary>
|
|
/// <typeparam name="T">The type produced by the observable.</typeparam>
|
|
/// <param name="source">The observable</param>
|
|
/// <returns>An <see cref="IBinding"/>.</returns>
|
|
public static IBinding ToBinding<T>(this IObservable<T> source)
|
|
{
|
|
return new BindingAdaptor(source.Select(x => (object)x));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an observable for a <see cref="AvaloniaProperty"/>.
|
|
/// </summary>
|
|
/// <param name="o">The object.</param>
|
|
/// <param name="property">The property.</param>
|
|
/// <returns>
|
|
/// An observable which fires immediately with the current value of the property on the
|
|
/// object and subsequently each time the property value changes.
|
|
/// </returns>
|
|
public static IObservable<object> GetObservable(this IAvaloniaObject o, AvaloniaProperty property)
|
|
{
|
|
Contract.Requires<ArgumentNullException>(o != null);
|
|
Contract.Requires<ArgumentNullException>(property != null);
|
|
|
|
return new AvaloniaObservable<object>(
|
|
observer =>
|
|
{
|
|
EventHandler<AvaloniaPropertyChangedEventArgs> handler = (s, e) =>
|
|
{
|
|
if (e.Property == property)
|
|
{
|
|
observer.OnNext(e.NewValue);
|
|
}
|
|
};
|
|
|
|
observer.OnNext(o.GetValue(property));
|
|
|
|
o.PropertyChanged += handler;
|
|
|
|
return Disposable.Create(() =>
|
|
{
|
|
o.PropertyChanged -= handler;
|
|
});
|
|
},
|
|
GetDescription(o, property));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an observable for a <see cref="AvaloniaProperty"/>.
|
|
/// </summary>
|
|
/// <param name="o">The object.</param>
|
|
/// <typeparam name="T">The property type.</typeparam>
|
|
/// <param name="property">The property.</param>
|
|
/// <returns>
|
|
/// An observable which fires immediately with the current value of the property on the
|
|
/// object and subsequently each time the property value changes.
|
|
/// </returns>
|
|
public static IObservable<T> GetObservable<T>(this IAvaloniaObject o, AvaloniaProperty<T> property)
|
|
{
|
|
Contract.Requires<ArgumentNullException>(o != null);
|
|
Contract.Requires<ArgumentNullException>(property != null);
|
|
|
|
return o.GetObservable((AvaloniaProperty)property).Cast<T>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an observable for a <see cref="AvaloniaProperty"/>.
|
|
/// </summary>
|
|
/// <param name="o">The object.</param>
|
|
/// <typeparam name="T">The type of the property.</typeparam>
|
|
/// <param name="property">The property.</param>
|
|
/// <returns>
|
|
/// An observable which when subscribed pushes the old and new values of the property each
|
|
/// time it is changed. Note that the observable returned from this method does not fire
|
|
/// with the current value of the property immediately.
|
|
/// </returns>
|
|
public static IObservable<Tuple<T, T>> GetObservableWithHistory<T>(
|
|
this IAvaloniaObject o,
|
|
AvaloniaProperty<T> property)
|
|
{
|
|
Contract.Requires<ArgumentNullException>(o != null);
|
|
Contract.Requires<ArgumentNullException>(property != null);
|
|
|
|
return new AvaloniaObservable<Tuple<T, T>>(
|
|
observer =>
|
|
{
|
|
EventHandler<AvaloniaPropertyChangedEventArgs> handler = (s, e) =>
|
|
{
|
|
if (e.Property == property)
|
|
{
|
|
observer.OnNext(Tuple.Create((T)e.OldValue, (T)e.NewValue));
|
|
}
|
|
};
|
|
|
|
o.PropertyChanged += handler;
|
|
|
|
return Disposable.Create(() =>
|
|
{
|
|
o.PropertyChanged -= handler;
|
|
});
|
|
},
|
|
GetDescription(o, property));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a subject for a <see cref="AvaloniaProperty"/>.
|
|
/// </summary>
|
|
/// <param name="o">The object.</param>
|
|
/// <param name="property">The property.</param>
|
|
/// <param name="priority">
|
|
/// The priority with which binding values are written to the object.
|
|
/// </param>
|
|
/// <returns>
|
|
/// An <see cref="ISubject{Object}"/> which can be used for two-way binding to/from the
|
|
/// property.
|
|
/// </returns>
|
|
public static ISubject<object> GetSubject(
|
|
this IAvaloniaObject o,
|
|
AvaloniaProperty property,
|
|
BindingPriority priority = BindingPriority.LocalValue)
|
|
{
|
|
// TODO: Subject.Create<T> is not yet in stable Rx : once it is, remove the
|
|
// AnonymousSubject classes and use Subject.Create<T>.
|
|
var output = new Subject<object>();
|
|
var result = new AnonymousSubject<object>(
|
|
Observer.Create<object>(
|
|
x => output.OnNext(x),
|
|
e => output.OnError(e),
|
|
() => output.OnCompleted()),
|
|
o.GetObservable(property));
|
|
o.Bind(property, output, priority);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a subject for a <see cref="AvaloniaProperty"/>.
|
|
/// </summary>
|
|
/// <typeparam name="T">The property type.</typeparam>
|
|
/// <param name="o">The object.</param>
|
|
/// <param name="property">The property.</param>
|
|
/// <param name="priority">
|
|
/// The priority with which binding values are written to the object.
|
|
/// </param>
|
|
/// <returns>
|
|
/// An <see cref="ISubject{T}"/> which can be used for two-way binding to/from the
|
|
/// property.
|
|
/// </returns>
|
|
public static ISubject<T> GetSubject<T>(
|
|
this IAvaloniaObject o,
|
|
AvaloniaProperty<T> property,
|
|
BindingPriority priority = BindingPriority.LocalValue)
|
|
{
|
|
// TODO: Subject.Create<T> is not yet in stable Rx : once it is, remove the
|
|
// AnonymousSubject classes from this file and use Subject.Create<T>.
|
|
var output = new Subject<T>();
|
|
var result = new AnonymousSubject<T>(
|
|
Observer.Create<T>(
|
|
x => output.OnNext(x),
|
|
e => output.OnError(e),
|
|
() => output.OnCompleted()),
|
|
o.GetObservable(property));
|
|
o.Bind(property, output, priority);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a weak observable for a <see cref="AvaloniaProperty"/>.
|
|
/// </summary>
|
|
/// <param name="o">The object.</param>
|
|
/// <param name="property">The property.</param>
|
|
/// <returns>An observable.</returns>
|
|
public static IObservable<object> GetWeakObservable(this IAvaloniaObject o, AvaloniaProperty property)
|
|
{
|
|
Contract.Requires<ArgumentNullException>(o != null);
|
|
Contract.Requires<ArgumentNullException>(property != null);
|
|
|
|
return new WeakPropertyChangedObservable(
|
|
new WeakReference<IAvaloniaObject>(o),
|
|
property,
|
|
GetDescription(o, property));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Binds a property on an <see cref="IAvaloniaObject"/> to an <see cref="IBinding"/>.
|
|
/// </summary>
|
|
/// <param name="target">The object.</param>
|
|
/// <param name="property">The property to bind.</param>
|
|
/// <param name="binding">The binding.</param>
|
|
/// <param name="anchor">
|
|
/// An optional anchor from which to locate required context. When binding to objects that
|
|
/// are not in the logical tree, certain types of binding need an anchor into the tree in
|
|
/// order to locate named controls or resources. The <paramref name="anchor"/> parameter
|
|
/// can be used to provice this context.
|
|
/// </param>
|
|
/// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns>
|
|
public static IDisposable Bind(
|
|
this IAvaloniaObject target,
|
|
AvaloniaProperty property,
|
|
IBinding binding,
|
|
object anchor = null)
|
|
{
|
|
Contract.Requires<ArgumentNullException>(target != null);
|
|
Contract.Requires<ArgumentNullException>(property != null);
|
|
Contract.Requires<ArgumentNullException>(binding != null);
|
|
|
|
var metadata = property.GetMetadata(target.GetType()) as IDirectPropertyMetadata;
|
|
|
|
var result = binding.Initiate(
|
|
target,
|
|
property,
|
|
anchor,
|
|
metadata?.EnableDataValidation ?? false);
|
|
|
|
if (result != null)
|
|
{
|
|
return BindingOperations.Apply(target, property, result, anchor);
|
|
}
|
|
else
|
|
{
|
|
return Disposable.Empty;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subscribes to a property changed notifications for changes that originate from a
|
|
/// <typeparamref name="TTarget"/>.
|
|
/// </summary>
|
|
/// <typeparam name="TTarget">The type of the property change sender.</typeparam>
|
|
/// <param name="observable">The property changed observable.</param>
|
|
/// <param name="action">
|
|
/// The method to call. The parameters are the sender and the event args.
|
|
/// </param>
|
|
/// <returns>A disposable that can be used to terminate the subscription.</returns>
|
|
public static IDisposable AddClassHandler<TTarget>(
|
|
this IObservable<AvaloniaPropertyChangedEventArgs> observable,
|
|
Action<TTarget, AvaloniaPropertyChangedEventArgs> action)
|
|
where TTarget : AvaloniaObject
|
|
{
|
|
return observable.Subscribe(e =>
|
|
{
|
|
if (e.Sender is TTarget)
|
|
{
|
|
action((TTarget)e.Sender, e);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subscribes to a property changed notifications for changes that originate from a
|
|
/// <typeparamref name="TTarget"/>.
|
|
/// </summary>
|
|
/// <typeparam name="TTarget">The type of the property change sender.</typeparam>
|
|
/// <param name="observable">The property changed observable.</param>
|
|
/// <param name="handler">Given a TTarget, returns the handler.</param>
|
|
/// <returns>A disposable that can be used to terminate the subscription.</returns>
|
|
public static IDisposable AddClassHandler<TTarget>(
|
|
this IObservable<AvaloniaPropertyChangedEventArgs> observable,
|
|
Func<TTarget, Action<AvaloniaPropertyChangedEventArgs>> handler)
|
|
where TTarget : class
|
|
{
|
|
return observable.Subscribe(e => SubscribeAdapter(e, handler));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a description of a property that van be used in observables.
|
|
/// </summary>
|
|
/// <param name="o">The object.</param>
|
|
/// <param name="property">The property</param>
|
|
/// <returns>The description.</returns>
|
|
private static string GetDescription(IAvaloniaObject o, AvaloniaProperty property)
|
|
{
|
|
return $"{o.GetType().Name}.{property.Name}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Observer method for <see cref="AddClassHandler{TTarget}(IObservable{AvaloniaPropertyChangedEventArgs},
|
|
/// Func{TTarget, Action{AvaloniaPropertyChangedEventArgs}})"/>.
|
|
/// </summary>
|
|
/// <typeparam name="TTarget">The sender type to accept.</typeparam>
|
|
/// <param name="e">The event args.</param>
|
|
/// <param name="handler">Given a TTarget, returns the handler.</param>
|
|
private static void SubscribeAdapter<TTarget>(
|
|
AvaloniaPropertyChangedEventArgs e,
|
|
Func<TTarget, Action<AvaloniaPropertyChangedEventArgs>> handler)
|
|
where TTarget : class
|
|
{
|
|
var target = e.Sender as TTarget;
|
|
|
|
if (target != null)
|
|
{
|
|
handler(target)(e);
|
|
}
|
|
}
|
|
|
|
private class BindingAdaptor : IBinding
|
|
{
|
|
private IObservable<object> _source;
|
|
|
|
public BindingAdaptor(IObservable<object> source)
|
|
{
|
|
this._source = source;
|
|
}
|
|
|
|
public InstancedBinding Initiate(
|
|
IAvaloniaObject target,
|
|
AvaloniaProperty targetProperty,
|
|
object anchor = null,
|
|
bool enableDataValidation = false)
|
|
{
|
|
return new InstancedBinding(_source);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|