diff --git a/samples/BindingTest/MainWindow.paml b/samples/BindingTest/MainWindow.paml index 3a378fcd24..903d1f1050 100644 --- a/samples/BindingTest/MainWindow.paml +++ b/samples/BindingTest/MainWindow.paml @@ -6,7 +6,7 @@ - + @@ -30,6 +30,11 @@ + + + + diff --git a/src/Markup/Perspex.Markup.Xaml/Context/NameScopeWrapper.cs b/src/Markup/Perspex.Markup.Xaml/Context/NameScopeWrapper.cs new file mode 100644 index 0000000000..4cb67ce912 --- /dev/null +++ b/src/Markup/Perspex.Markup.Xaml/Context/NameScopeWrapper.cs @@ -0,0 +1,30 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Perspex.Markup.Xaml.Context +{ + internal class NameScopeWrapper : OmniXaml.INameScope + { + private Perspex.INameScope _inner; + + public NameScopeWrapper(Perspex.INameScope inner) + { + _inner = inner; + } + + public object Find(string name) + { + return _inner.Find(name); + } + + public void Register(string name, object scopedElement) + { + _inner.Register(name, scopedElement); + } + + public void Unregister(string name) + { + _inner.Unregister(name); + } + } +} diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs index 30f2b5ca7c..89203022a9 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using System.Reflection; using Glass; @@ -11,21 +9,17 @@ using OmniXaml; using OmniXaml.Builder; using OmniXaml.TypeConversion; using OmniXaml.Typing; +using Perspex.Collections; using Perspex.Controls; +using Perspex.Controls.Primitives; +using Perspex.Controls.Templates; using Perspex.Input; -using Perspex.Markup.Xaml.Templates; using Perspex.Markup.Xaml.Converters; -using Perspex.Markup.Xaml.Data; -using Perspex.Markup.Xaml.MarkupExtensions; using Perspex.Media; using Perspex.Media.Imaging; using Perspex.Metadata; using Perspex.Platform; using Perspex.Styling; -using Perspex.Controls.Primitives; -using Perspex.Controls.Templates; -using System.Collections; -using Perspex.Collections; namespace Perspex.Markup.Xaml.Context { @@ -48,9 +42,10 @@ namespace Perspex.Markup.Xaml.Context var xamlNamespaceRegistry = CreateXamlNamespaceRegistry(); var typeRepository = new PerspexTypeRepository(xamlNamespaceRegistry, typeFactory, featureProvider); - typeRepository.RegisterMetadata(new Metadata().WithMemberDependency(x => x.Value, x => x.Property)); + typeRepository.RegisterMetadata(new GenericMetadata().WithRuntimeNameProperty(d => d.Name)); + typeRepository.RegisterMetadata(new GenericMetadata().WithMemberDependency(x => x.Value, x => x.Property)); typeRepository.RegisterMetadata( - new Metadata() + new GenericMetadata() .WithMemberDependency(x => x.SelectedIndex, x => x.Items) .WithMemberDependency(x => x.SelectedItem, x => x.Items)); diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs index 34f0dcca67..c771b30509 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs @@ -18,6 +18,28 @@ namespace Perspex.Markup.Xaml.Context { } + public override OmniXaml.INameScope GetNamescope(object instance) + { + var result = this.UnderlyingType as OmniXaml.INameScope; + + if (result == null) + { + var visual = instance as Visual; + + if (visual != null) + { + var perspexNs = (instance as Perspex.INameScope) ?? NameScope.GetNameScope(visual); + + if (perspexNs != null) + { + result = new NameScopeWrapper(perspexNs); + } + } + } + + return result; + } + protected override XamlMember LookupMember(string name) { return new PerspexXamlMember(name, this, TypeRepository, FeatureProvider); diff --git a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs index 8cc5a981f5..08d7e82387 100644 --- a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs @@ -4,7 +4,6 @@ using System; using System.Reactive.Linq; using System.Reactive.Subjects; -using OmniXaml.TypeConversion; using Perspex.Controls; using Perspex.Markup.Data; @@ -20,6 +19,11 @@ namespace Perspex.Markup.Xaml.Data /// public IValueConverter Converter { 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 binding mode. /// @@ -38,7 +42,7 @@ namespace Perspex.Markup.Xaml.Data /// /// Gets or sets the binding path. /// - public string SourcePropertyPath { get; set; } + public string Path { get; set; } /// /// Applies the binding to a property on an instance. @@ -47,8 +51,11 @@ namespace Perspex.Markup.Xaml.Data /// The target property. public void Bind(IObservablePropertyBag instance, PerspexProperty property) { + Contract.Requires(instance != null); + Contract.Requires(property != null); + var subject = CreateSubject( - instance, + instance, property.PropertyType, property == Control.DataContextProperty); @@ -72,15 +79,33 @@ namespace Perspex.Markup.Xaml.Data Type targetType, bool targetIsDataContext = false) { + Contract.Requires(target != null); + Contract.Requires(targetType != null); + + var pathInfo = ParsePath(Path); + ValidateState(pathInfo); + ExpressionObserver observer; - if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext) + if (pathInfo.ElementName != null || ElementName != null) { - observer = CreateDataContextExpressionSubject(target, targetIsDataContext); + observer = CreateElementSubject( + (IControl)target, + pathInfo.ElementName ?? ElementName, + pathInfo.Path); + } + else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext) + { + observer = CreateDataContextSubject( + target, + pathInfo.Path, + targetIsDataContext); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { - observer = CreateTemplatedParentExpressionSubject(target); + observer = CreateTemplatedParentSubject( + target, + pathInfo.Path); } else { @@ -88,8 +113,8 @@ namespace Perspex.Markup.Xaml.Data } return new ExpressionSubject( - observer, - targetType, + observer, + targetType, Converter ?? DefaultValueConverter.Instance); } @@ -101,6 +126,10 @@ namespace Perspex.Markup.Xaml.Data /// The binding subject. internal void Bind(IObservablePropertyBag target, PerspexProperty property, ISubject subject) { + Contract.Requires(target != null); + Contract.Requires(property != null); + Contract.Requires(subject != null); + var mode = Mode == BindingMode.Default ? property.DefaultBindingMode : Mode; @@ -117,7 +146,7 @@ namespace Perspex.Markup.Xaml.Data target.GetObservable(Control.DataContextProperty).Subscribe(dataContext => { subject.Take(1).Subscribe(x => target.SetValue(property, x, Priority)); - }); + }); break; case BindingMode.OneWayToSource: target.GetObservable(property).Subscribe(subject); @@ -125,10 +154,60 @@ namespace Perspex.Markup.Xaml.Data } } - private ExpressionObserver CreateDataContextExpressionSubject( + private static PathInfo ParsePath(string path) + { + var result = new PathInfo(); + + if (string.IsNullOrWhiteSpace(path) || path == ".") + { + result.Path = string.Empty; + } + else if (path.StartsWith("#")) + { + var dot = path.IndexOf('.'); + + if (dot != -1) + { + result.Path = path.Substring(dot + 1); + result.ElementName = path.Substring(1, dot - 1); + } + else + { + result.Path = string.Empty; + result.ElementName = path.Substring(1); + } + } + else + { + result.Path = path; + } + + return result; + } + + private void ValidateState(PathInfo pathInfo) + { + if (pathInfo.ElementName != null && ElementName != null) + { + throw new InvalidOperationException( + "ElementName property cannot be set when an #elementName path is provided."); + } + + if ((pathInfo.ElementName != null || ElementName != null) && + RelativeSource != null) + { + throw new InvalidOperationException( + "ElementName property cannot be set with a RelativeSource."); + } + } + + private ExpressionObserver CreateDataContextSubject( IObservablePropertyBag target, + string path, bool targetIsDataContext) { + Contract.Requires(target != null); + var dataContextHost = targetIsDataContext ? target.InheritanceParent as IObservablePropertyBag : target; @@ -136,7 +215,7 @@ namespace Perspex.Markup.Xaml.Data { var result = new ExpressionObserver( () => dataContextHost.GetValue(Control.DataContextProperty), - GetExpression()); + path); dataContextHost.GetObservable(Control.DataContextProperty).Subscribe(x => result.UpdateRoot()); return result; @@ -148,11 +227,15 @@ namespace Perspex.Markup.Xaml.Data } } - private ExpressionObserver CreateTemplatedParentExpressionSubject(IObservablePropertyBag target) + private ExpressionObserver CreateTemplatedParentSubject( + IObservablePropertyBag target, + string path) { + Contract.Requires(target != null); + var result = new ExpressionObserver( () => target.GetValue(Control.TemplatedParentProperty), - GetExpression()); + path); if (target.GetValue(Control.TemplatedParentProperty) == null) { @@ -167,10 +250,38 @@ namespace Perspex.Markup.Xaml.Data return result; } - private string GetExpression() + private ExpressionObserver CreateElementSubject( + IControl target, + string elementName, + string path) + { + Contract.Requires(target != null); + + var result = new ExpressionObserver( + ControlLocator.Track(target, elementName), + path); + return result; + } + + private IControl LookupNamedControl(IControl target) + { + Contract.Requires(target != null); + + var nameScope = target.FindNameScope(); + + if (nameScope == null) + { + throw new InvalidOperationException( + "Could not find name scope for ElementName binding."); + } + + return nameScope.Find(ElementName); + } + + private class PathInfo { - return SourcePropertyPath == null || SourcePropertyPath == "." ? - string.Empty : SourcePropertyPath; + public string Path { get; set; } + public string ElementName { get; set; } } } } \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 4c5f8bc283..c2cdc5efd3 100644 --- a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -22,12 +22,14 @@ namespace Perspex.Markup.Xaml.MarkupExtensions return new Binding { Converter = Converter, + ElementName = ElementName, Mode = Mode, - SourcePropertyPath = Path, + Path = Path, }; } public IValueConverter Converter { get; set; } + public string ElementName { get; set; } public BindingMode Mode { get; set; } public string Path { get; set; } } diff --git a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs index 0fdd5339cf..b3bd1c7c75 100644 --- a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs +++ b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs @@ -19,17 +19,19 @@ namespace Perspex.Markup.Xaml.MarkupExtensions public override object ProvideValue(MarkupExtensionContext extensionContext) { - return new Data.Binding + return new Binding { Converter = Converter, + ElementName = ElementName, Mode = Mode, Priority = BindingPriority.TemplatedParent, RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), - SourcePropertyPath = Path, + Path = Path, }; } public IValueConverter Converter { get; set; } + public string ElementName { get; set; } public BindingMode Mode { get; set; } public string Path { get; set; } } diff --git a/src/Markup/Perspex.Markup.Xaml/OmniXAML b/src/Markup/Perspex.Markup.Xaml/OmniXAML index 55fc2abc71..3e3b46ba66 160000 --- a/src/Markup/Perspex.Markup.Xaml/OmniXAML +++ b/src/Markup/Perspex.Markup.Xaml/OmniXAML @@ -1 +1 @@ -Subproject commit 55fc2abc715885d3686f8c8605f34714445cd72f +Subproject commit 3e3b46ba66941da925092e2977003d0553cfc907 diff --git a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index cebb76826f..35fde7d0e4 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -38,6 +38,7 @@ Properties\SharedAssemblyInfo.cs + @@ -185,7 +186,6 @@ - @@ -205,11 +205,15 @@ + + + + diff --git a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs index 6672727014..dc9143be1b 100644 --- a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -35,7 +35,7 @@ namespace Perspex.Markup.Xaml.Templates { if (ItemsSource != null) { - var obs = new ExpressionObserver(item, ItemsSource.SourcePropertyPath); + var obs = new ExpressionObserver(item, ItemsSource.Path); return obs.Take(1).Wait() as IEnumerable; } diff --git a/src/Markup/Perspex.Markup/ControlLocator.cs b/src/Markup/Perspex.Markup/ControlLocator.cs new file mode 100644 index 0000000000..36330d89c5 --- /dev/null +++ b/src/Markup/Perspex.Markup/ControlLocator.cs @@ -0,0 +1,60 @@ +// Copyright (c) The Perspex 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.Linq; +using Perspex.Controls; + +namespace Perspex.Markup +{ + /// + /// Locates controls relative to other controls. + /// + public static class ControlLocator + { + /// + /// Tracks a named control relative to another control. + /// + /// + /// The control relative from which the other control should be found. + /// + /// The name of the control to find. + public static IObservable Track(IControl relativeTo, string name) + { + var attached = Observable.FromEventPattern( + x => relativeTo.AttachedToVisualTree += x, + x => relativeTo.DetachedFromVisualTree += x) + .Select(x => x.EventArgs.NameScope) + .StartWith(relativeTo.FindNameScope()); + + var detached = Observable.FromEventPattern( + x => relativeTo.DetachedFromVisualTree += x, + x => relativeTo.DetachedFromVisualTree += x) + .Select(x => (INameScope)null); + + return attached.Merge(detached).Select(nameScope => + { + if (nameScope != null) + { + var registered = Observable.FromEventPattern( + x => nameScope.Registered += x, + x => nameScope.Registered -= x) + .Where(x => x.EventArgs.Name == name) + .Select(x => x.EventArgs.Element) + .OfType(); + var unregistered = Observable.FromEventPattern( + x => nameScope.Unregistered += x, + x => nameScope.Unregistered -= x); + return registered + .StartWith(nameScope.Find(name)) + .TakeUntil(unregistered); + } + else + { + return Observable.Return(null); + } + }).Switch(); + } + } +} diff --git a/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs index 8f1d247e55..d669713156 100644 --- a/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs @@ -26,9 +26,12 @@ namespace Perspex.Markup.Data new InpcPropertyAccessorPlugin(), }; - private Func _root; + private readonly object _root; + private readonly Func _rootGetter; + private readonly IObservable _rootObservable; + private IDisposable _rootObserverSubscription; private int _count; - private ExpressionNode _node; + private readonly ExpressionNode _node; private ISubject _empty; /// @@ -37,21 +40,50 @@ namespace Perspex.Markup.Data /// The root object. /// The expression. public ExpressionObserver(object root, string expression) - : this(() => root, expression) { + Contract.Requires(expression != null); + + _root = root; + + if (!string.IsNullOrWhiteSpace(expression)) + { + _node = ExpressionNodeBuilder.Build(expression); + } + + Expression = expression; } /// /// Initializes a new instance of the class. /// - /// A function which gets the root object. + /// An observable which provides the root object. /// The expression. - public ExpressionObserver(Func root, string expression) + public ExpressionObserver(IObservable rootObservable, string expression) { - Contract.Requires(root != null); + Contract.Requires(rootObservable != null); Contract.Requires(expression != null); - _root = root; + _rootObservable = rootObservable; + + if (!string.IsNullOrWhiteSpace(expression)) + { + _node = ExpressionNodeBuilder.Build(expression); + } + + Expression = expression; + } + + /// + /// Initializes a new instance of the class. + /// + /// A function which gets the root object. + /// The expression. + public ExpressionObserver(Func rootGetter, string expression) + { + Contract.Requires(rootGetter != null); + Contract.Requires(expression != null); + + _rootGetter = rootGetter; if (!string.IsNullOrWhiteSpace(expression)) { @@ -105,9 +137,13 @@ namespace Perspex.Markup.Data { return (Leaf as PropertyAccessorNode)?.PropertyType; } + else if(_rootGetter != null) + { + return _rootGetter()?.GetType(); + } else { - return _root()?.GetType(); + return _root?.GetType(); } } finally @@ -134,19 +170,19 @@ namespace Perspex.Markup.Data } /// - /// Causes the root object to be re-read. + /// Causes the root object to be re-read from the root getter. /// public void UpdateRoot() { - if (_count > 0) + if (_count > 0 && _rootGetter != null) { if (_node != null) { - _node.Target = _root(); + _node.Target = _rootGetter(); } else if (_empty != null) { - _empty.OnNext(_root()); + _empty.OnNext(_rootGetter()); } } } @@ -170,7 +206,7 @@ namespace Perspex.Markup.Data { if (_empty == null) { - _empty = new BehaviorSubject(_root()); + _empty = new BehaviorSubject(_rootGetter()); } return _empty.Subscribe(observer); @@ -181,7 +217,18 @@ namespace Perspex.Markup.Data { if (_count++ == 0 && _node != null) { - _node.Target = _root(); + if (_rootGetter != null) + { + _node.Target = _rootGetter(); + } + else if (_rootObservable != null) + { + _rootObserverSubscription = _rootObservable.Subscribe(x => _node.Target = x); + } + else + { + _node.Target = _root; + } } } @@ -189,6 +236,12 @@ namespace Perspex.Markup.Data { if (--_count == 0 && _node != null) { + if (_rootObserverSubscription != null) + { + _rootObserverSubscription.Dispose(); + _rootObserverSubscription = null; + } + _node.Target = null; } } diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj index 414ec0dee2..0fc06f2499 100644 --- a/src/Markup/Perspex.Markup/Perspex.Markup.csproj +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -42,6 +42,7 @@ + @@ -91,10 +92,30 @@ {b09b78d8-9b26-48b0-9149-d64a2f120f3f} Perspex.Base + + {d2221c82-4a25-4583-9b43-d791e3f6820c} + Perspex.Controls + + + {62024b2d-53eb-4638-b26b-85eeaa54866e} + Perspex.Input + + + {6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b} + Perspex.Interactivity + + + {42472427-4774-4c81-8aff-9f27b8e31721} + Perspex.Layout + {eb582467-6abb-43a1-b052-e981ba910e3a} Perspex.SceneGraph + + {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} + Perspex.Styling +