// 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.Linq;
using System.Reactive;
using System.Reactive.Linq;
using Avalonia.Data.Converters;
using Avalonia.Data.Core;
using Avalonia.LogicalTree;
using Avalonia.Markup.Parsers;
using Avalonia.Reactive;
using Avalonia.VisualTree;
namespace Avalonia.Data
{
///
/// A XAML binding.
///
public class Binding : IBinding
{
///
/// Initializes a new instance of the class.
///
public Binding()
{
FallbackValue = AvaloniaProperty.UnsetValue;
}
///
/// Initializes a new instance of the class.
///
/// The binding path.
/// The binding mode.
public Binding(string path, BindingMode mode = BindingMode.Default)
: this()
{
Path = path;
Mode = mode;
}
///
/// Gets or sets the to use.
///
public IValueConverter Converter { get; set; }
///
/// Gets or sets a parameter to pass to .
///
public object ConverterParameter { get; set; }
///
/// Gets or sets the name of the element to use as the binding source.
///
public string ElementName { get; set; }
///
/// Gets or sets the value to use when the binding is unable to produce a value.
///
public object FallbackValue { get; set; }
///
/// Gets or sets the binding mode.
///
public BindingMode Mode { get; set; }
///
/// Gets or sets the binding path.
///
public string Path { get; set; } = "";
///
/// Gets or sets the binding priority.
///
public BindingPriority Priority { get; set; }
///
/// Gets or sets the relative source for the binding.
///
public RelativeSource RelativeSource { get; set; }
///
/// Gets or sets the source for the binding.
///
public object Source { get; set; }
///
/// Gets or sets the string format.
///
public string StringFormat { get; set; }
public WeakReference DefaultAnchor { get; set; }
///
/// Gets or sets a function used to resolve types from names in the binding path.
///
public Func TypeResolver { get; set; }
///
public InstancedBinding Initiate(
IAvaloniaObject target,
AvaloniaProperty targetProperty,
object anchor = null,
bool enableDataValidation = false)
{
Contract.Requires(target != null);
anchor = anchor ?? DefaultAnchor?.Target;
enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue;
ExpressionObserver observer;
var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver);
if (ElementName != null)
{
observer = CreateElementObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
ElementName,
node);
}
else if (Source != null)
{
observer = CreateSourceObserver(Source, node);
}
else if (RelativeSource == null)
{
if (mode == SourceMode.Data)
{
observer = CreateDataContextObserver(
target,
node,
targetProperty == StyledElement.DataContextProperty,
anchor);
}
else
{
observer = new ExpressionObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
node);
}
}
else if (RelativeSource.Mode == RelativeSourceMode.DataContext)
{
observer = CreateDataContextObserver(
target,
node,
targetProperty == StyledElement.DataContextProperty,
anchor);
}
else if (RelativeSource.Mode == RelativeSourceMode.Self)
{
observer = CreateSourceObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
node);
}
else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent)
{
observer = CreateTemplatedParentObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
node);
}
else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor)
{
if (RelativeSource.Tree == TreeType.Visual && RelativeSource.AncestorType == null)
{
throw new InvalidOperationException("AncestorType must be set for RelativeSourceMode.FindAncestor when searching the visual tree.");
}
observer = CreateFindAncestorObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
RelativeSource,
node);
}
else
{
throw new NotSupportedException();
}
var fallback = FallbackValue;
// If we're binding to DataContext and our fallback is UnsetValue then override
// the fallback value to null, as broken bindings to DataContext must reset the
// DataContext in order to not propagate incorrect DataContexts to child controls.
// See Avalonia.Markup.UnitTests.Data.DataContext_Binding_Should_Produce_Correct_Results.
if (targetProperty == StyledElement.DataContextProperty && fallback == AvaloniaProperty.UnsetValue)
{
fallback = null;
}
var converter = Converter;
var targetType = targetProperty?.PropertyType ?? typeof(object);
// We only respect `StringFormat` if the type of the property we're assigning to will
// accept a string. Note that this is slightly different to WPF in that WPF only applies
// `StringFormat` for target type `string` (not `object`).
if (!string.IsNullOrWhiteSpace(StringFormat) &&
(targetType == typeof(string) || targetType == typeof(object)))
{
converter = new StringFormatValueConverter(StringFormat, converter);
}
var subject = new BindingExpression(
observer,
targetType,
fallback,
converter ?? DefaultValueConverter.Instance,
ConverterParameter,
Priority);
return new InstancedBinding(subject, Mode, Priority);
}
private ExpressionObserver CreateDataContextObserver(
IAvaloniaObject target,
ExpressionNode node,
bool targetIsDataContext,
object anchor)
{
Contract.Requires(target != null);
if (!(target is IStyledElement))
{
target = anchor as IStyledElement;
if (target == null)
{
throw new InvalidOperationException("Cannot find a DataContext to bind to.");
}
}
if (!targetIsDataContext)
{
var result = new ExpressionObserver(
() => target.GetValue(StyledElement.DataContextProperty),
node,
new UpdateSignal(target, StyledElement.DataContextProperty),
null);
return result;
}
else
{
return new ExpressionObserver(
GetParentDataContext(target),
node,
null);
}
}
private ExpressionObserver CreateElementObserver(
IStyledElement target,
string elementName,
ExpressionNode node)
{
Contract.Requires(target != null);
var result = new ExpressionObserver(
ControlLocator.Track(target, elementName),
node,
null);
return result;
}
private ExpressionObserver CreateFindAncestorObserver(
IStyledElement target,
RelativeSource relativeSource,
ExpressionNode node)
{
Contract.Requires(target != null);
IObservable