Browse Source

Merge branch 'master' into 9650-fix-nullable-bool

pull/9725/head
Max Katz 4 years ago
committed by GitHub
parent
commit
98e55c63d6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 45
      src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
  2. 53
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs
  3. 53
      src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs
  4. 9
      src/Avalonia.Controls/NativeMenuBar.cs
  5. 17
      src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml
  6. 21
      src/Avalonia.Themes.Simple/Controls/NativeMenuBar.xaml
  7. 3
      tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
  8. 115
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  9. 44
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs

45
src/Avalonia.Base/PropertyStore/BindingEntryBase.cs

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Threading;
namespace Avalonia.PropertyStore namespace Avalonia.PropertyStore
{ {
@ -116,26 +117,42 @@ namespace Avalonia.PropertyStore
private void SetValue(BindingValue<TValue> value) private void SetValue(BindingValue<TValue> value)
{ {
if (Frame.Owner is null) static void Execute(BindingEntryBase<TValue, TSource> instance, BindingValue<TValue> value)
return; {
if (instance.Frame.Owner is null)
return;
LoggingUtils.LogIfNecessary(Frame.Owner.Owner, Property, value); LoggingUtils.LogIfNecessary(instance.Frame.Owner.Owner, instance.Property, value);
if (value.HasValue) if (value.HasValue)
{ {
if (!_hasValue || !EqualityComparer<TValue>.Default.Equals(_value, value.Value)) if (!instance._hasValue || !EqualityComparer<TValue>.Default.Equals(instance._value, value.Value))
{
instance._value = value.Value;
instance._hasValue = true;
if (instance._subscription is not null && instance._subscription != s_creatingQuiet)
instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority);
}
}
else if (value.Type != BindingValueType.DoNothing)
{ {
_value = value.Value; instance.ClearValue();
_hasValue = true; if (instance._subscription is not null && instance._subscription != s_creatingQuiet)
if (_subscription is not null && _subscription != s_creatingQuiet) instance.Frame.Owner?.OnBindingValueCleared(instance.Property, instance.Frame.Priority);
Frame.Owner?.OnBindingValueChanged(this, Frame.Priority);
} }
} }
else if (value.Type != BindingValueType.DoNothing)
if (Dispatcher.UIThread.CheckAccess())
{ {
ClearValue(); Execute(this, value);
if (_subscription is not null && _subscription != s_creatingQuiet) }
Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority); else
{
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var instance = this;
var newValue = value;
Dispatcher.UIThread.Post(() => Execute(instance, newValue));
} }
} }

53
src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Threading;
namespace Avalonia.PropertyStore namespace Avalonia.PropertyStore
{ {
@ -40,20 +41,56 @@ namespace Avalonia.PropertyStore
public void OnNext(T value) public void OnNext(T value)
{ {
if (Property.ValidateValue?.Invoke(value) != false) static void Execute(ValueStore owner, StyledPropertyBase<T> property, T value)
_owner.SetValue(Property, value, BindingPriority.LocalValue); {
if (property.ValidateValue?.Invoke(value) != false)
owner.SetValue(property, value, BindingPriority.LocalValue);
else
owner.ClearLocalValue(property);
}
if (Dispatcher.UIThread.CheckAccess())
{
Execute(_owner, Property, value);
}
else else
_owner.ClearLocalValue(Property); {
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var instance = _owner;
var property = Property;
var newValue = value;
Dispatcher.UIThread.Post(() => Execute(instance, property, newValue));
}
} }
public void OnNext(BindingValue<T> value) public void OnNext(BindingValue<T> value)
{ {
LoggingUtils.LogIfNecessary(_owner.Owner, Property, value); static void Execute(LocalValueBindingObserver<T> instance, BindingValue<T> value)
{
var owner = instance._owner;
var property = instance.Property;
LoggingUtils.LogIfNecessary(owner.Owner, property, value);
if (value.HasValue) if (value.HasValue)
_owner.SetValue(Property, value.Value, BindingPriority.LocalValue); owner.SetValue(property, value.Value, BindingPriority.LocalValue);
else if (value.Type != BindingValueType.DataValidationError) else if (value.Type != BindingValueType.DataValidationError)
_owner.ClearLocalValue(Property); owner.ClearLocalValue(property);
}
if (Dispatcher.UIThread.CheckAccess())
{
Execute(this, value);
}
else
{
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var instance = this;
var newValue = value;
Dispatcher.UIThread.Post(() => Execute(instance, newValue));
}
} }
} }
} }

53
src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs

@ -1,5 +1,7 @@
using System; using System;
using System.Security.Cryptography;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Threading;
namespace Avalonia.PropertyStore namespace Avalonia.PropertyStore
{ {
@ -34,28 +36,47 @@ namespace Avalonia.PropertyStore
public void OnNext(object? value) public void OnNext(object? value)
{ {
if (value is BindingNotification n) static void Execute(LocalValueUntypedBindingObserver<T> instance, object? value)
{ {
value = n.Value; var owner = instance._owner;
LoggingUtils.LogIfNecessary(_owner.Owner, Property, n); var property = instance.Property;
}
if (value == AvaloniaProperty.UnsetValue) if (value is BindingNotification n)
{ {
_owner.ClearLocalValue(Property); value = n.Value;
} LoggingUtils.LogIfNecessary(owner.Owner, property, n);
else if (value == BindingOperations.DoNothing) }
{
// Do nothing! if (value == AvaloniaProperty.UnsetValue)
{
owner.ClearLocalValue(property);
}
else if (value == BindingOperations.DoNothing)
{
// Do nothing!
}
else if (UntypedValueUtils.TryConvertAndValidate(property, value, out var typedValue))
{
owner.SetValue(property, typedValue, BindingPriority.LocalValue);
}
else
{
owner.ClearLocalValue(property);
LoggingUtils.LogInvalidValue(owner.Owner, property, typeof(T), value);
}
} }
else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue))
if (Dispatcher.UIThread.CheckAccess())
{ {
_owner.SetValue(Property, typedValue, BindingPriority.LocalValue); Execute(this, value);
} }
else else if (value != BindingOperations.DoNothing)
{ {
_owner.ClearLocalValue(Property); // To avoid allocating closure in the outer scope we need to capture variables
LoggingUtils.LogInvalidValue(_owner.Owner, Property, typeof(T), value); // locally. This allows us to skip most of the allocations when on UI thread.
var instance = this;
var newValue = value;
Dispatcher.UIThread.Post(() => Execute(instance, newValue));
} }
} }
} }

9
src/Avalonia.Controls/NativeMenuBar.cs

@ -23,15 +23,6 @@ namespace Avalonia.Controls
}); });
} }
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NativeMenu))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NativeMenuItem))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NativeMenuItemBase))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NativeMenuItemSeparator))]
public NativeMenuBar()
{
}
public static void SetEnableMenuItemClickForwarding(MenuItem menuItem, bool enable) public static void SetEnableMenuItemClickForwarding(MenuItem menuItem, bool enable)
{ {
menuItem.SetValue(EnableMenuItemClickForwardingProperty, enable); menuItem.SetValue(EnableMenuItemClickForwardingProperty, enable);

17
src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml

@ -9,17 +9,16 @@
IsVisible="{Binding !$parent[TopLevel].(NativeMenu.IsNativeMenuExported)}" IsVisible="{Binding !$parent[TopLevel].(NativeMenu.IsNativeMenuExported)}"
Items="{Binding $parent[TopLevel].(NativeMenu.Menu).Items}"> Items="{Binding $parent[TopLevel].(NativeMenu.Menu).Items}">
<Menu.Styles> <Menu.Styles>
<!-- Don't use x:DataType and compiled bindings here, as it might crash https://github.com/AvaloniaUI/Avalonia/pull/7954 --> <Style Selector="MenuItem" x:DataType="NativeMenuItem">
<Style Selector="MenuItem"> <Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Header" Value="{ReflectionBinding Header}"/> <Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
<Setter Property="IsEnabled" Value="{ReflectionBinding IsEnabled}"/> <Setter Property="InputGesture" Value="{Binding Gesture}"/>
<Setter Property="InputGesture" Value="{ReflectionBinding Gesture}"/> <Setter Property="Items" Value="{Binding Menu.Items}"/>
<Setter Property="Items" Value="{ReflectionBinding Menu.Items}"/> <Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="Command" Value="{ReflectionBinding Command}"/> <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
<Setter Property="CommandParameter" Value="{ReflectionBinding CommandParameter}"/>
<Setter Property="(NativeMenuBar.EnableMenuItemClickForwarding)" Value="True"/> <Setter Property="(NativeMenuBar.EnableMenuItemClickForwarding)" Value="True"/>
<!--NativeMenuItem is IBitmap and MenuItem is Image--> <!--NativeMenuItem is IBitmap and MenuItem is Image-->
<Setter Property="Icon" Value="{ReflectionBinding Icon , Converter={StaticResource AvaloniaThemesFluentNativeMenuBarIBitmapToImageConverter}}"/> <Setter Property="Icon" Value="{Binding Icon , Converter={StaticResource AvaloniaThemesFluentNativeMenuBarIBitmapToImageConverter}}"/>
</Style> </Style>
</Menu.Styles> </Menu.Styles>
</Menu> </Menu>

21
src/Avalonia.Themes.Simple/Controls/NativeMenuBar.xaml

@ -9,17 +9,16 @@
<Menu IsVisible="{Binding !$parent[TopLevel].(NativeMenu.IsNativeMenuExported)}" <Menu IsVisible="{Binding !$parent[TopLevel].(NativeMenu.IsNativeMenuExported)}"
Items="{Binding $parent[TopLevel].(NativeMenu.Menu).Items}"> Items="{Binding $parent[TopLevel].(NativeMenu.Menu).Items}">
<Menu.Styles> <Menu.Styles>
<!-- Don't use x:DataType and compiled bindings here, as it might crash https://github.com/AvaloniaUI/Avalonia/pull/7954 --> <Style Selector="MenuItem" x:DataType="NativeMenuItem">
<Style Selector="MenuItem"> <Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Header" Value="{ReflectionBinding Header}" /> <Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
<Setter Property="IsEnabled" Value="{ReflectionBinding IsEnabled}" /> <Setter Property="InputGesture" Value="{Binding Gesture}"/>
<Setter Property="InputGesture" Value="{ReflectionBinding Gesture}" /> <Setter Property="Items" Value="{Binding Menu.Items}"/>
<Setter Property="Items" Value="{ReflectionBinding Menu.Items}" /> <Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="Command" Value="{ReflectionBinding Command}" /> <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
<Setter Property="CommandParameter" Value="{ReflectionBinding CommandParameter}" /> <Setter Property="(NativeMenuBar.EnableMenuItemClickForwarding)" Value="True"/>
<Setter Property="(NativeMenuBar.EnableMenuItemClickForwarding)" Value="True" /> <!--NativeMenuItem is IBitmap and MenuItem is Image-->
<!-- NativeMenuItem is IBitmap and MenuItem is Image --> <Setter Property="Icon" Value="{Binding Icon , Converter={StaticResource AvaloniaThemesSimpleNativeMenuBarIBitmapToImageConverter}}"/>
<Setter Property="Icon" Value="{ReflectionBinding Icon, Converter={StaticResource AvaloniaThemesSimpleNativeMenuBarIBitmapToImageConverter}}" />
</Style> </Style>
</Menu.Styles> </Menu.Styles>
</Menu> </Menu>

3
tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj

@ -17,6 +17,9 @@
<EmbeddedResource Remove="..\Avalonia.RenderTests\Assets\NotoColorEmoji.ttf" /> <EmbeddedResource Remove="..\Avalonia.RenderTests\Assets\NotoColorEmoji.ttf" />
<EmbeddedResource Include="Media\TextFormatting\BreakPairTable.txt" /> <EmbeddedResource Include="Media\TextFormatting\BreakPairTable.txt" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Nito.AsyncEx.Context" Version="5.1.2" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" /> <ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj" /> <ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj" />

115
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@ -12,6 +12,7 @@ using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Moq; using Moq;
using Nito.AsyncEx;
using Xunit; using Xunit;
#nullable enable #nullable enable
@ -920,6 +921,120 @@ namespace Avalonia.Base.UnitTests
} }
} }
[Theory]
[InlineData(BindingPriority.LocalValue)]
[InlineData(BindingPriority.Style)]
public void Typed_Bind_Executes_On_UIThread(BindingPriority priority)
{
AsyncContext.Run(async () =>
{
var target = new Class1();
var source = new Subject<string>();
var currentThreadId = Thread.CurrentThread.ManagedThreadId;
var raised = 0;
var threadingInterfaceMock = new Mock<IPlatformThreadingInterface>();
threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
.Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
var services = new TestServices(
threadingInterface: threadingInterfaceMock.Object);
target.PropertyChanged += (s, e) =>
{
Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId);
++raised;
};
using (UnitTestApplication.Start(services))
{
target.Bind(Class1.FooProperty, source, priority);
await Task.Run(() => source.OnNext("foobar"));
Dispatcher.UIThread.RunJobs();
Assert.Equal("foobar", target.GetValue(Class1.FooProperty));
Assert.Equal(1, raised);
}
});
}
[Theory]
[InlineData(BindingPriority.LocalValue)]
[InlineData(BindingPriority.Style)]
public void Untyped_Bind_Executes_On_UIThread(BindingPriority priority)
{
AsyncContext.Run(async () =>
{
var target = new Class1();
var source = new Subject<object>();
var currentThreadId = Thread.CurrentThread.ManagedThreadId;
var raised = 0;
var threadingInterfaceMock = new Mock<IPlatformThreadingInterface>();
threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
.Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
var services = new TestServices(
threadingInterface: threadingInterfaceMock.Object);
target.PropertyChanged += (s, e) =>
{
Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId);
++raised;
};
using (UnitTestApplication.Start(services))
{
target.Bind(Class1.FooProperty, source, priority);
await Task.Run(() => source.OnNext("foobar"));
Dispatcher.UIThread.RunJobs();
Assert.Equal("foobar", target.GetValue(Class1.FooProperty));
Assert.Equal(1, raised);
}
});
}
[Theory]
[InlineData(BindingPriority.LocalValue)]
[InlineData(BindingPriority.Style)]
public void BindingValue_Bind_Executes_On_UIThread(BindingPriority priority)
{
AsyncContext.Run(async () =>
{
var target = new Class1();
var source = new Subject<BindingValue<string>>();
var currentThreadId = Thread.CurrentThread.ManagedThreadId;
var raised = 0;
var threadingInterfaceMock = new Mock<IPlatformThreadingInterface>();
threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
.Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
var services = new TestServices(
threadingInterface: threadingInterfaceMock.Object);
target.PropertyChanged += (s, e) =>
{
Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId);
++raised;
};
using (UnitTestApplication.Start(services))
{
target.Bind(Class1.FooProperty, source, priority);
await Task.Run(() => source.OnNext("foobar"));
Dispatcher.UIThread.RunJobs();
Assert.Equal("foobar", target.GetValue(Class1.FooProperty));
Assert.Equal(1, raised);
}
});
}
[Fact] [Fact]
public async Task Bind_With_Scheduler_Executes_On_Scheduler() public async Task Bind_With_Scheduler_Executes_On_Scheduler()
{ {

44
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs

@ -7,8 +7,10 @@ using System.Threading.Tasks;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Moq; using Moq;
using Nito.AsyncEx;
using Xunit; using Xunit;
namespace Avalonia.Base.UnitTests namespace Avalonia.Base.UnitTests
@ -519,25 +521,39 @@ namespace Avalonia.Base.UnitTests
} }
[Fact] [Fact]
public async Task Bind_Executes_On_UIThread() public void Bind_Executes_On_UIThread()
{ {
var target = new Class1(); AsyncContext.Run(async () =>
var source = new Subject<object>(); {
var currentThreadId = Thread.CurrentThread.ManagedThreadId; var target = new Class1();
var source = new Subject<object>();
var currentThreadId = Thread.CurrentThread.ManagedThreadId;
var raised = 0;
var threadingInterfaceMock = new Mock<IPlatformThreadingInterface>(); var threadingInterfaceMock = new Mock<IPlatformThreadingInterface>();
threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
.Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
var services = new TestServices( var services = new TestServices(
threadingInterface: threadingInterfaceMock.Object); threadingInterface: threadingInterfaceMock.Object);
using (UnitTestApplication.Start(services)) target.PropertyChanged += (s, e) =>
{ {
target.Bind(Class1.FooProperty, source); Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId);
++raised;
};
await Task.Run(() => source.OnNext("foobar")); using (UnitTestApplication.Start(services))
} {
target.Bind(Class1.FooProperty, source);
await Task.Run(() => source.OnNext("foobar"));
Dispatcher.UIThread.RunJobs();
Assert.Equal("foobar", target.Foo);
Assert.Equal(1, raised);
}
});
} }
[Fact] [Fact]

Loading…
Cancel
Save