diff --git a/src/Markup/Perspex.Markup/Data/LogicalNotNode.cs b/src/Markup/Perspex.Markup/Data/LogicalNotNode.cs index 2447aee4f2..a2941684b4 100644 --- a/src/Markup/Perspex.Markup/Data/LogicalNotNode.cs +++ b/src/Markup/Perspex.Markup/Data/LogicalNotNode.cs @@ -11,7 +11,7 @@ namespace Perspex.Markup.Data { public override bool SetValue(object value) { - throw new NotSupportedException("Cannot set a negated binding."); + return false; } public override IDisposable Subscribe(IObserver observer) diff --git a/src/Markup/Perspex.Markup/DefaultValueConverter.cs b/src/Markup/Perspex.Markup/DefaultValueConverter.cs index e49c1c3ae6..23bb0f774c 100644 --- a/src/Markup/Perspex.Markup/DefaultValueConverter.cs +++ b/src/Markup/Perspex.Markup/DefaultValueConverter.cs @@ -3,6 +3,8 @@ using System; using System.Globalization; +using System.Linq; +using System.Reflection; using Perspex.Utilities; namespace Perspex.Markup @@ -30,7 +32,9 @@ namespace Perspex.Markup { object result; - if (value != null && TypeUtilities.TryConvert(targetType, value, culture, out result)) + if (value != null && + (TypeUtilities.TryConvert(targetType, value, culture, out result) || + TryConvertEnum(value, targetType, culture, out result))) { return result; } @@ -52,5 +56,34 @@ namespace Perspex.Markup { return Convert(value, targetType, parameter, culture); } + + private bool TryConvertEnum(object value, Type targetType, CultureInfo cultur, out object result) + { + var valueTypeInfo = value.GetType().GetTypeInfo(); + var targetTypeInfo = targetType.GetTypeInfo(); + + if (valueTypeInfo.IsEnum && !targetTypeInfo.IsEnum) + { + var enumValue = (int)value; + + if (TypeUtilities.TryCast(targetType, enumValue, out result)) + { + return true; + } + } + else if (!valueTypeInfo.IsEnum && targetTypeInfo.IsEnum) + { + object intValue; + + if (TypeUtilities.TryCast(typeof(int), value, out intValue)) + { + result = Enum.ToObject(targetType, intValue); + return true; + } + } + + result = null; + return false; + } } } diff --git a/src/Perspex.Base/DirectProperty.cs b/src/Perspex.Base/DirectProperty.cs index c926f28240..7ae19ec520 100644 --- a/src/Perspex.Base/DirectProperty.cs +++ b/src/Perspex.Base/DirectProperty.cs @@ -24,11 +24,13 @@ namespace Perspex /// The name of the property. /// Gets the current value of the property. /// Sets the value of the property. May be null. + /// The property metadata. public DirectProperty( string name, Func getter, - Action setter = null) - : base(name, typeof(TOwner), new PropertyMetadata()) + Action setter, + PropertyMetadata metadata) + : base(name, typeof(TOwner), metadata) { Contract.Requires(getter != null); diff --git a/src/Perspex.Base/PerspexProperty.cs b/src/Perspex.Base/PerspexProperty.cs index 4c0b6fd724..8998627849 100644 --- a/src/Perspex.Base/PerspexProperty.cs +++ b/src/Perspex.Base/PerspexProperty.cs @@ -342,16 +342,19 @@ namespace Perspex /// The name of the property. /// Gets the current value of the property. /// Sets the value of the property. + /// The default binding mode for the property. /// A public static DirectProperty RegisterDirect( string name, Func getter, - Action setter = null) + Action setter = null, + BindingMode defaultBindingMode = BindingMode.OneWay) where TOwner : IPerspexObject { Contract.Requires(name != null); - var result = new DirectProperty(name, getter, setter); + var metadata = new PropertyMetadata(defaultBindingMode); + var result = new DirectProperty(name, getter, setter, metadata); PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; } diff --git a/src/Perspex.Base/Utilities/TypeUtilities.cs b/src/Perspex.Base/Utilities/TypeUtilities.cs index 3d19de5f97..c6d16d56b2 100644 --- a/src/Perspex.Base/Utilities/TypeUtilities.cs +++ b/src/Perspex.Base/Utilities/TypeUtilities.cs @@ -119,6 +119,12 @@ namespace Perspex.Utilities return true; } + if (to == typeof(string)) + { + result = Convert.ToString(value); + return true; + } + if (to.GetTypeInfo().IsEnum && from == typeof(string)) { if (Enum.IsDefined(to, (string)value)) @@ -131,9 +137,7 @@ namespace Perspex.Utilities bool containsFrom = Conversions.ContainsKey(from); bool containsTo = Conversions.ContainsKey(to); - if ((containsFrom && containsTo) || - (from == typeof(string) && containsTo) || - (to == typeof(string) && containsFrom)) + if ((containsFrom && containsTo) || (from == typeof(string) && containsTo)) { try { diff --git a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs index 85628e3374..490ee8899b 100644 --- a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs @@ -52,6 +52,13 @@ namespace Perspex.Controls.Generators /// The removed containers. IEnumerable Dematerialize(int startingIndex, int count); + /// + /// Inserts space for newly inserted containers in the index. + /// + /// The index at which space should be inserted. + /// The number of blank spaces to create. + void InsertSpace(int index, int count); + /// /// Removes a set of created containers and updates the index of later containers to fill /// the gap. diff --git a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs index 2733de5770..f1f49affbf 100644 --- a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs @@ -85,6 +85,12 @@ namespace Perspex.Controls.Generators return result; } + /// + public virtual void InsertSpace(int index, int count) + { + _containers.InsertRange(index, Enumerable.Repeat(null, count)); + } + /// public virtual IEnumerable RemoveRange(int startingIndex, int count) { diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index 9feaab3d25..11a56aa556 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -68,6 +68,7 @@ namespace Perspex.Controls.Generators } else if (container != null) { + Index.Add(item, container); return container; } else diff --git a/src/Perspex.Controls/Presenters/ItemsPresenter.cs b/src/Perspex.Controls/Presenters/ItemsPresenter.cs index 29e796fd75..909a1e011f 100644 --- a/src/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/src/Perspex.Controls/Presenters/ItemsPresenter.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using Perspex.Controls.Generators; using Perspex.Controls.Templates; +using Perspex.Controls.Utils; using Perspex.Input; using Perspex.Styling; @@ -237,6 +238,11 @@ namespace Perspex.Controls.Presenters switch (e.Action) { case NotifyCollectionChangedAction.Add: + if (e.NewStartingIndex + e.NewItems.Count < this.Items.Count()) + { + generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count); + } + AddContainers(generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector)); break; @@ -281,7 +287,17 @@ namespace Perspex.Controls.Presenters { if (i.ContainerControl != null) { - this.Panel.Children.Add(i.ContainerControl); + if (i.Index < this.Panel.Children.Count) + { + // HACK: This will insert at the wrong place when there are null items, + // but all of this will need to be rewritten when we implement + // virtualization so hope no-one notices until then :) + this.Panel.Children.Insert(i.Index, i.ContainerControl); + } + else + { + this.Panel.Children.Add(i.ContainerControl); + } } } } diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 9ec708dd6b..f6e405a24a 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -62,7 +62,7 @@ namespace Perspex.Controls.Primitives PerspexProperty.RegisterDirect( nameof(SelectedItem), o => o.SelectedItem, - (o, v) => o.SelectedItem = v); + (o, v) => o.SelectedItem = v, BindingMode.TwoWay); /// /// Defines the property. diff --git a/src/Perspex.Controls/Primitives/TemplatedControl.cs b/src/Perspex.Controls/Primitives/TemplatedControl.cs index 7bf2edfbe7..10e312bb2b 100644 --- a/src/Perspex.Controls/Primitives/TemplatedControl.cs +++ b/src/Perspex.Controls/Primitives/TemplatedControl.cs @@ -251,7 +251,15 @@ namespace Perspex.Controls.Primitives { if (!_templateApplied) { - VisualChildren.Clear(); + if (VisualChildren.Count > 0) + { + foreach (var child in this.GetTemplateChildren()) + { + child.SetValue(TemplatedParentProperty, null); + } + + VisualChildren.Clear(); + } if (Template != null) { @@ -318,6 +326,11 @@ namespace Perspex.Controls.Primitives /// The event args. protected virtual void OnTemplateChanged(PerspexPropertyChangedEventArgs e) { + if (_templateApplied && VisualChildren.Count > 0) + { + _templateApplied = false; + } + _templateApplied = false; InvalidateMeasure(); } diff --git a/src/Perspex.Controls/Primitives/ToggleButton.cs b/src/Perspex.Controls/Primitives/ToggleButton.cs index 8d464464ec..c0aedf00ba 100644 --- a/src/Perspex.Controls/Primitives/ToggleButton.cs +++ b/src/Perspex.Controls/Primitives/ToggleButton.cs @@ -3,6 +3,7 @@ using System; using Perspex.Interactivity; +using Perspex.Data; namespace Perspex.Controls.Primitives { @@ -12,7 +13,7 @@ namespace Perspex.Controls.Primitives PerspexProperty.RegisterDirect( "IsChecked", o => o.IsChecked, - (o,v) => o.IsChecked = v); + (o,v) => o.IsChecked = v, BindingMode.TwoWay); private bool _isChecked; diff --git a/src/Perspex.Controls/TreeViewItem.cs b/src/Perspex.Controls/TreeViewItem.cs index fe74ebfd93..d5369840ad 100644 --- a/src/Perspex.Controls/TreeViewItem.cs +++ b/src/Perspex.Controls/TreeViewItem.cs @@ -20,7 +20,7 @@ namespace Perspex.Controls /// Defines the property. /// public static readonly StyledProperty IsExpandedProperty = - PerspexProperty.Register("IsExpanded"); + PerspexProperty.Register("IsExpanded", default(bool), false, Data.BindingMode.TwoWay); /// /// Defines the property. @@ -30,7 +30,7 @@ namespace Perspex.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel - { + { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, }); @@ -40,7 +40,7 @@ namespace Perspex.Controls /// Initializes static members of the class. /// static TreeViewItem() - { + { SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(true); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); diff --git a/tests/Perspex.Base.UnitTests/DirectPropertyTests.cs b/tests/Perspex.Base.UnitTests/DirectPropertyTests.cs index 68e1c62cf3..9e23e59f80 100644 --- a/tests/Perspex.Base.UnitTests/DirectPropertyTests.cs +++ b/tests/Perspex.Base.UnitTests/DirectPropertyTests.cs @@ -30,7 +30,11 @@ namespace Perspex.Base.UnitTests [Fact] public void IsDirect_Property_Returns_True() { - var target = new DirectProperty("test", o => null); + var target = new DirectProperty( + "test", + o => null, + null, + new PropertyMetadata()); Assert.True(target.IsDirect); } diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index dd7b41caee..10737ad4d1 100644 --- a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -147,6 +147,26 @@ namespace Perspex.Controls.UnitTests.Presenters Assert.Equal(new[] { "foo", "baz", "bar" }, text); } + [Fact] + public void Inserting_Items_Should_Update_Containers() + { + var items = new ObservableCollection { "foo", "bar", "baz" }; + var target = new ItemsPresenter + { + Items = items, + }; + + target.ApplyTemplate(); + items.Insert(2, "insert"); + + var text = target.Panel.Children + .OfType() + .Select(x => x.Text) + .ToList(); + + Assert.Equal(new[] { "foo", "bar", "insert", "baz" }, text); + } + [Fact] public void Setting_Items_To_Null_Should_Remove_Containers() { diff --git a/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs index 4de5d8fd3b..a93c6b3026 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -311,6 +311,39 @@ namespace Perspex.Controls.UnitTests.Primitives Assert.True(raised); } + [Fact] + public void Applying_New_Template_Clears_TemplatedParent_Of_Old_Template_Children() + { + var target = new TestTemplatedControl + { + Template = new FuncControlTemplate(_ => new Decorator + { + Child = new Border(), + }) + }; + + target.ApplyTemplate(); + + var decorator = (Decorator)target.GetVisualChildren().Single(); + var border = (Border)decorator.Child; + + Assert.Equal(target, decorator.TemplatedParent); + Assert.Equal(target, border.TemplatedParent); + + target.Template = new FuncControlTemplate(_ => new Canvas()); + + // Templated children should not be removed here: the control may be re-added + // somewhere with the same template, so they could still be of use. + Assert.Same(decorator, target.GetVisualChildren().Single()); + Assert.Equal(target, decorator.TemplatedParent); + Assert.Equal(target, border.TemplatedParent); + + target.ApplyTemplate(); + + Assert.Null(decorator.TemplatedParent); + Assert.Null(border.TemplatedParent); + } + private static IControl ScrollingContentControlTemplate(ContentControl control) { return new Border diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs index 3581a71563..9dc40ece00 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs @@ -81,12 +81,12 @@ namespace Perspex.Markup.UnitTests.Data } [Fact] - public void SetValue_Should_Throw() + public void SetValue_Should_Return_False() { var data = new { Foo = "foo" }; var target = new ExpressionObserver(data, "!Foo"); - Assert.Throws(() => target.SetValue("bar")); + Assert.False(target.SetValue("bar")); } } } diff --git a/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs b/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs index b438d6af1d..55034a3c53 100644 --- a/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs +++ b/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Globalization; +using Perspex.Controls; using Xunit; namespace Perspex.Markup.UnitTests @@ -44,6 +45,18 @@ namespace Perspex.Markup.UnitTests Assert.Equal(TestEnum.Bar, result); } + [Fact] + public void Can_Convert_Int_To_Enum() + { + var result = DefaultValueConverter.Instance.Convert( + 1, + typeof(TestEnum), + null, + CultureInfo.InvariantCulture); + + Assert.Equal(TestEnum.Bar, result); + } + [Fact] public void Can_Convert_Double_To_String() { @@ -57,15 +70,27 @@ namespace Perspex.Markup.UnitTests } [Fact] - public void Can_Convert_Double_To_Int() + public void Can_Convert_Enum_To_Int() { var result = DefaultValueConverter.Instance.Convert( - 5.0, + TestEnum.Bar, typeof(int), null, CultureInfo.InvariantCulture); - Assert.Equal(5, result); + Assert.Equal(1, result); + } + + [Fact] + public void Can_Convert_Enum_To_String() + { + var result = DefaultValueConverter.Instance.Convert( + TestEnum.Bar, + typeof(string), + null, + CultureInfo.InvariantCulture); + + Assert.Equal("Bar", result); } [Fact] @@ -80,6 +105,18 @@ namespace Perspex.Markup.UnitTests Assert.Equal(5.0, result); } + [Fact] + public void Cannot_Convert_Between_Different_Enum_Types() + { + var result = DefaultValueConverter.Instance.Convert( + TestEnum.Foo, + typeof(Orientation), + null, + CultureInfo.InvariantCulture); + + Assert.Equal(PerspexProperty.UnsetValue, result); + } + private enum TestEnum { Foo,