Browse Source

Merge branch 'master' into fixes/1099-inherited-propertychanged-order

pull/2264/head
Steven Kirk 8 years ago
parent
commit
c8cb447cca
  1. 17
      .github/PULL_REQUEST_TEMPLATE.md
  2. 31
      Avalonia.sln
  3. 2
      nukebuild/BuildParameters.cs
  4. 3
      scripts/ReplaceNugetCache.ps1
  5. 29
      src/Avalonia.Base/AvaloniaObject.cs
  6. 2
      src/Avalonia.Base/AvaloniaProperty.cs
  7. 81
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  8. 2
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  9. 76
      src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs
  10. 22
      src/Avalonia.Base/ISupportInitialize.cs
  11. 20
      src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs
  12. 144
      src/Avalonia.Base/ValueStore.cs
  13. 2
      src/Avalonia.Controls/AutoCompleteBox.cs
  14. 2
      src/Avalonia.Controls/Button.cs
  15. 1
      src/Avalonia.Controls/Control.cs
  16. 76
      src/Avalonia.Controls/DropDown.cs
  17. 1
      src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs
  18. 1
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs
  19. 17
      src/Avalonia.Controls/Image.cs
  20. 2
      src/Avalonia.Controls/ItemsControl.cs
  21. 4
      src/Avalonia.Controls/MenuItem.cs
  22. 2
      src/Avalonia.Controls/PixelPointEventArgs.cs
  23. 2
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  24. 13
      src/Avalonia.Controls/Shapes/Shape.cs
  25. 11
      src/Avalonia.Controls/TopLevel.cs
  26. 2
      src/Avalonia.Controls/Window.cs
  27. 4
      src/Avalonia.Controls/WindowBase.cs
  28. 2
      src/Avalonia.Native/ScreenImpl.cs
  29. 6
      src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs
  30. 4
      src/Avalonia.ReactiveUI/ReactiveUserControl.cs
  31. 224
      src/Avalonia.ReactiveUI/RoutedViewHost.cs
  32. 7
      src/Avalonia.Remote.Protocol/MetsysBson.cs
  33. 6
      src/Avalonia.Remote.Protocol/TcpTransportBase.cs
  34. 7
      src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs
  35. 72
      src/Avalonia.Styling/Styling/NotSelector.cs
  36. 11
      src/Avalonia.Styling/Styling/Selectors.cs
  37. 6
      src/Avalonia.Styling/Styling/Style.cs
  38. 18
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  39. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  40. 1
      src/Avalonia.X11/Avalonia.X11.csproj
  41. 263
      src/Avalonia.X11/NativeDialogs/Gtk.cs
  42. 122
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  43. 4
      src/Avalonia.X11/X11Platform.cs
  44. 3
      src/Avalonia.X11/X11Screens.cs
  45. 17
      src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs
  46. 35
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs
  47. 2
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs
  48. 2
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github
  49. 51
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
  50. 13
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
  51. 17
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs
  52. 20
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs
  53. 56
      tests/Avalonia.Controls.UnitTests/ImageTests.cs
  54. 1
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  55. 16
      tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
  56. 21
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  57. 62
      tests/Avalonia.DesignerSupport.Tests/Helpers.cs
  58. 171
      tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs
  59. 73
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
  60. 3
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs
  61. 28
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  62. 35
      tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs
  63. 104
      tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs
  64. 114
      tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs
  65. 1
      tests/Avalonia.Styling.UnitTests/StyledElementTests.cs

17
.github/PULL_REQUEST_TEMPLATE.md

@ -1,18 +1,18 @@
## What does the pull request do?
<!--- Give a bit of background on the PR here, together with links to with related issues etc. -->
Give a bit of background on the PR here, together with links to with related issues etc.
## What is the current behavior?
<!--- If the PR is a fix, describe the current incorrect behavior, otherwise delete this section. -->
If the PR is a fix, describe the current incorrect behavior, otherwise delete this section.
## What is the updated/expected behavior with this PR?
<!--- Describe how to test the PR. -->
Describe how to test the PR.
## How was the solution implemented (if it's not obvious)?
<!--- Include any information that might be of use to a reviewer here. -->
Include any information that might be of use to a reviewer here.
## Checklist
@ -21,12 +21,11 @@ Include any information that might be of use to a reviewer here.
- [ ] Consider submitting a PR to https://github.com/AvaloniaUI/Avaloniaui.net with user documentation
## Breaking changes
<!--- List any breaking changes here. When the PR is merged please add an entry to https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes -->
List any breaking changes here. When the PR is merged please add an entry to https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes
## Fixed issues
If the pull request fixes issue(s) list them like this:
<!--- If the pull request fixes issue(s) list them like this:
Fixes #123
Fixes #456
Fixes #456
-->

31
Avalonia.sln

@ -196,9 +196,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "nukebuild\_build.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Animation.UnitTests", "tests\Avalonia.Animation.UnitTests\Avalonia.Animation.UnitTests.csproj", "{AF227847-E65C-4BE9-BCE9-B551357788E0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.X11", "src\Avalonia.X11\Avalonia.X11.csproj", "{41B02319-965D-4945-8005-C1A3D1224165}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.X11", "src\Avalonia.X11\Avalonia.X11.csproj", "{41B02319-965D-4945-8005-C1A3D1224165}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlatformSanityChecks", "samples\PlatformSanityChecks\PlatformSanityChecks.csproj", "{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "samples\PlatformSanityChecks\PlatformSanityChecks.csproj", "{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
@ -1819,6 +1821,30 @@ Global
{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|iPhone.Build.0 = Release|Any CPU
{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhone.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhone.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|Any CPU.Build.0 = Release|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhone.ActiveCfg = Release|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhone.Build.0 = Release|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1875,6 +1901,7 @@ Global
{AF227847-E65C-4BE9-BCE9-B551357788E0} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

2
nukebuild/BuildParameters.cs

@ -109,7 +109,7 @@ public partial class Build
if (!IsNuGetRelease)
{
// Use AssemblyVersion with Build as version
Version += "-build" + Environment.GetEnvironmentVariable("BUILD_BUILDID") + "-beta";
Version += "-cibuild" + int.Parse(Environment.GetEnvironmentVariable("BUILD_BUILDID")).ToString("0000000") + "-beta";
}
PublishTestResults = true;

3
scripts/ReplaceNugetCache.ps1

@ -1,6 +1,5 @@
copy ..\samples\ControlCatalog.Desktop\bin\Debug\net461\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\net461\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Gtk3.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\

29
src/Avalonia.Base/AvaloniaObject.cs

@ -36,32 +36,7 @@ namespace Avalonia
public AvaloniaObject()
{
VerifyAccess();
void Notify(AvaloniaProperty property)
{
object value = property.IsDirect ?
((IDirectPropertyAccessor)property).GetValue(this) :
((IStyledPropertyAccessor)property).GetDefaultValue(GetType());
var e = new AvaloniaPropertyChangedEventArgs(
this,
property,
AvaloniaProperty.UnsetValue,
value,
BindingPriority.Unset);
property.NotifyInitialized(e);
}
foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegistered(this))
{
Notify(property);
}
foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(this.GetType()))
{
Notify(property);
}
AvaloniaPropertyRegistry.Instance.NotifyInitialized(this);
}
/// <summary>
@ -645,7 +620,7 @@ namespace Avalonia
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The default value.</returns>
internal object GetDefaultValue(AvaloniaProperty property)
private object GetDefaultValue(AvaloniaProperty property)
{
if (property.Inherits && InheritanceParent is AvaloniaObject aobj)
return aobj.GetValue(property);

2
src/Avalonia.Base/AvaloniaProperty.cs

@ -21,7 +21,7 @@ namespace Avalonia
/// </summary>
public static readonly object UnsetValue = new Unset();
private static int s_nextId = 1;
private static int s_nextId;
private readonly Subject<AvaloniaPropertyChangedEventArgs> _initialized;
private readonly Subject<AvaloniaPropertyChangedEventArgs> _changed;
private readonly PropertyMetadata _defaultMetadata;

81
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Data;
namespace Avalonia
{
@ -13,6 +14,8 @@ namespace Avalonia
/// </summary>
public class AvaloniaPropertyRegistry
{
private readonly Dictionary<int, AvaloniaProperty> _properties =
new Dictionary<int, AvaloniaProperty>();
private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _registered =
new Dictionary<Type, Dictionary<int, AvaloniaProperty>>();
private readonly Dictionary<Type, Dictionary<int, AvaloniaProperty>> _attached =
@ -21,6 +24,8 @@ namespace Avalonia
new Dictionary<Type, List<AvaloniaProperty>>();
private readonly Dictionary<Type, List<AvaloniaProperty>> _attachedCache =
new Dictionary<Type, List<AvaloniaProperty>>();
private readonly Dictionary<Type, List<KeyValuePair<AvaloniaProperty, object>>> _initializedCache =
new Dictionary<Type, List<KeyValuePair<AvaloniaProperty, object>>>();
/// <summary>
/// Gets the <see cref="AvaloniaPropertyRegistry"/> instance
@ -28,6 +33,11 @@ namespace Avalonia
public static AvaloniaPropertyRegistry Instance { get; }
= new AvaloniaPropertyRegistry();
/// <summary>
/// Gets a list of all registered properties.
/// </summary>
internal IReadOnlyCollection<AvaloniaProperty> Properties => _properties.Values;
/// <summary>
/// Gets all non-attached <see cref="AvaloniaProperty"/>s registered on a type.
/// </summary>
@ -148,6 +158,16 @@ namespace Avalonia
return FindRegistered(o.GetType(), name);
}
/// <summary>
/// Finds a registered property by Id.
/// </summary>
/// <param name="id">The property Id.</param>
/// <returns>The registered property or null if no matching property found.</returns>
internal AvaloniaProperty FindRegistered(int id)
{
return id < _properties.Count ? _properties[id] : null;
}
/// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is registered on a type.
/// </summary>
@ -202,8 +222,14 @@ namespace Avalonia
{
inner.Add(property.Id, property);
}
if (!_properties.ContainsKey(property.Id))
{
_properties.Add(property.Id, property);
}
_registeredCache.Clear();
_initializedCache.Clear();
}
/// <summary>
@ -237,8 +263,59 @@ namespace Avalonia
{
inner.Add(property.Id, property);
}
_attachedCache.Clear();
_initializedCache.Clear();
}
internal void NotifyInitialized(AvaloniaObject o)
{
Contract.Requires<ArgumentNullException>(o != null);
var type = o.GetType();
void Notify(AvaloniaProperty property, object value)
{
var e = new AvaloniaPropertyChangedEventArgs(
o,
property,
AvaloniaProperty.UnsetValue,
value,
BindingPriority.Unset);
property.NotifyInitialized(e);
}
if (!_initializedCache.TryGetValue(type, out var items))
{
var build = new Dictionary<AvaloniaProperty, object>();
foreach (var property in GetRegistered(type))
{
var value = !property.IsDirect ?
((IStyledPropertyAccessor)property).GetDefaultValue(type) :
null;
build.Add(property, value);
}
foreach (var property in GetRegisteredAttached(type))
{
if (!build.ContainsKey(property))
{
var value = ((IStyledPropertyAccessor)property).GetDefaultValue(type);
build.Add(property, value);
}
}
items = build.ToList();
_initializedCache.Add(type, items);
}
foreach (var i in items)
{
var value = i.Key.IsDirect ? o.GetValue(i.Key) : i.Value;
Notify(i.Key, value);
}
}
}
}

2
src/Avalonia.Base/Data/Core/BindingExpression.cs

@ -177,7 +177,7 @@ namespace Avalonia.Data.Core
protected override void Subscribed(IObserver<object> observer, bool first)
{
if (!first && _value != null && _value.TryGetTarget(out var val) == true)
if (!first && _value != null && _value.TryGetTarget(out var val))
{
observer.OnNext(val);
}

76
src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs

@ -2,6 +2,9 @@
// 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 System.Reflection;
namespace Avalonia.Data.Core.Plugins
{
@ -10,12 +13,19 @@ namespace Avalonia.Data.Core.Plugins
/// </summary>
public class ObservableStreamPlugin : IStreamPlugin
{
static MethodInfo observableSelect;
/// <summary>
/// Checks whether this plugin handles the specified value.
/// </summary>
/// <param name="reference">A weak reference to the value.</param>
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
public virtual bool Match(WeakReference reference) => reference.Target is IObservable<object>;
public virtual bool Match(WeakReference reference)
{
return reference.Target.GetType().GetInterfaces().Any(x =>
x.IsGenericType &&
x.GetGenericTypeDefinition() == typeof(IObservable<>));
}
/// <summary>
/// Starts producing output based on the specified value.
@ -26,7 +36,69 @@ namespace Avalonia.Data.Core.Plugins
/// </returns>
public virtual IObservable<object> Start(WeakReference reference)
{
return reference.Target as IObservable<object>;
var target = reference.Target;
// If the observable returns a reference type then we can cast it.
if (target is IObservable<object> result)
{
return result;
};
// If the observable returns a value type then we need to call Observable.Select on it.
// First get the type of T in `IObservable<T>`.
var sourceType = reference.Target.GetType().GetInterfaces().First(x =>
x.IsGenericType &&
x.GetGenericTypeDefinition() == typeof(IObservable<>)).GetGenericArguments()[0];
// Get the Observable.Select method.
var select = GetObservableSelect(sourceType);
// Make a Box<> delegate of the correct type.
var funcType = typeof(Func<,>).MakeGenericType(sourceType, typeof(object));
var box = GetType().GetMethod(nameof(Box), BindingFlags.Static | BindingFlags.NonPublic)
.MakeGenericMethod(sourceType)
.CreateDelegate(funcType);
// Call Observable.Select(target, box);
return (IObservable<object>)select.Invoke(
null,
new object[] { target, box });
}
private static MethodInfo GetObservableSelect(Type source)
{
return GetObservableSelect().MakeGenericMethod(source, typeof(object));
}
private static MethodInfo GetObservableSelect()
{
if (observableSelect == null)
{
observableSelect = typeof(Observable).GetRuntimeMethods().First(x =>
{
if (x.Name == nameof(Observable.Select) &&
x.ContainsGenericParameters &&
x.GetGenericArguments().Length == 2)
{
var parameters = x.GetParameters();
if (parameters.Length == 2 &&
parameters[0].ParameterType.IsConstructedGenericType &&
parameters[0].ParameterType.GetGenericTypeDefinition() == typeof(IObservable<>) &&
parameters[1].ParameterType.IsConstructedGenericType &&
parameters[1].ParameterType.GetGenericTypeDefinition() == typeof(Func<,>))
{
return true;
}
}
return false;
});
}
return observableSelect;
}
private static object Box<T>(T value) => (object)value;
}
}

22
src/Avalonia.Base/ISupportInitialize.cs

@ -1,22 +0,0 @@
// 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.
namespace Avalonia
{
/// <summary>
/// Specifies that this object supports a simple, transacted notification for batch
/// initialization.
/// </summary>
public interface ISupportInitialize
{
/// <summary>
/// Signals the object that initialization is starting.
/// </summary>
void BeginInit();
/// <summary>
/// Signals the object that initialization is complete.
/// </summary>
void EndInit();
}
}

20
src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs

@ -4,6 +4,8 @@ using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Xml.Linq;
using System.Linq;
// ReSharper disable AssignNullToNotNullAttribute
@ -19,10 +21,20 @@ namespace Avalonia.Utilities
{
var ver = new BinaryReader(stream).ReadInt32();
if (ver > LastKnownVersion)
throw new Exception("Resources index format version is not known");
var index = (AvaloniaResourcesIndex)
new DataContractSerializer(typeof(AvaloniaResourcesIndex)).ReadObject(stream);
return index.Entries;
throw new Exception("Resources index format version is not known");
var assetDoc = XDocument.Load(stream);
XNamespace assetNs = assetDoc.Root.Attribute("xmlns").Value;
List<AvaloniaResourcesIndexEntry> entries=
(from entry in assetDoc.Root.Element(assetNs + "Entries").Elements(assetNs + "AvaloniaResourcesIndexEntry")
select new AvaloniaResourcesIndexEntry
{
Path = entry.Element(assetNs + "Path").Value,
Offset = int.Parse(entry.Element(assetNs + "Offset").Value),
Size = int.Parse(entry.Element(assetNs + "Size").Value)
}).ToList();
return entries;
}
public static void Write(Stream stream, List<AvaloniaResourcesIndexEntry> entries)

144
src/Avalonia.Base/ValueStore.cs

@ -7,13 +7,21 @@ namespace Avalonia
{
internal class ValueStore : IPriorityValueOwner
{
private struct Entry
{
internal int PropertyId;
internal object Value;
}
private readonly AvaloniaObject _owner;
private readonly Dictionary<AvaloniaProperty, object> _values =
new Dictionary<AvaloniaProperty, object>();
private Entry[] _entries;
public ValueStore(AvaloniaObject owner)
{
_owner = owner;
// The last item in the list is always int.MaxValue
_entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } };
}
public IDisposable AddBinding(
@ -23,7 +31,7 @@ namespace Avalonia
{
PriorityValue priorityValue;
if (_values.TryGetValue(property, out var v))
if (TryGetValue(property, out var v))
{
priorityValue = v as PriorityValue;
@ -31,13 +39,13 @@ namespace Avalonia
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
_values[property] = priorityValue;
SetValueInternal(property, priorityValue);
}
}
else
{
priorityValue = CreatePriorityValue(property);
_values.Add(property, priorityValue);
AddValueInternal(property, priorityValue);
}
return priorityValue.Add(source, (int)priority);
@ -47,7 +55,7 @@ namespace Avalonia
{
PriorityValue priorityValue;
if (_values.TryGetValue(property, out var v))
if (TryGetValue(property, out var v))
{
priorityValue = v as PriorityValue;
@ -55,7 +63,7 @@ namespace Avalonia
{
if (priority == (int)BindingPriority.LocalValue)
{
_values[property] = Validate(property, value);
SetValueInternal(property, Validate(property, value));
Changed(property, priority, v, value);
return;
}
@ -63,7 +71,7 @@ namespace Avalonia
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
_values[property] = priorityValue;
SetValueInternal(property, priorityValue);
}
}
}
@ -76,14 +84,14 @@ namespace Avalonia
if (priority == (int)BindingPriority.LocalValue)
{
_values.Add(property, Validate(property, value));
AddValueInternal(property, Validate(property, value));
Changed(property, priority, AvaloniaProperty.UnsetValue, value);
return;
}
else
{
priorityValue = CreatePriorityValue(property);
_values.Add(property, priorityValue);
AddValueInternal(property, priorityValue);
}
}
@ -100,6 +108,16 @@ namespace Avalonia
_owner.PriorityValueChanged(property, priority, oldValue, newValue);
}
public IDictionary<AvaloniaProperty, object> GetSetValues()
{
var dict = new Dictionary<AvaloniaProperty, object>(_entries.Length - 1);
for (int i = 0; i < _entries.Length - 1; ++i)
{
dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
}
return dict;
}
public void LogError(AvaloniaProperty property, Exception e)
{
_owner.LogBindingError(property, e);
@ -111,7 +129,7 @@ namespace Avalonia
{
var result = AvaloniaProperty.UnsetValue;
if (_values.TryGetValue(property, out var value))
if (TryGetValue(property, out var value))
{
result = (value is PriorityValue priorityValue) ? priorityValue.Value : value;
}
@ -121,12 +139,12 @@ namespace Avalonia
public bool IsAnimating(AvaloniaProperty property)
{
return _values.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
}
public bool IsSet(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var value))
if (TryGetValue(property, out var value))
{
return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue;
}
@ -136,7 +154,7 @@ namespace Avalonia
public void Revalidate(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var value))
if (TryGetValue(property, out var value))
{
(value as PriorityValue)?.Revalidate();
}
@ -183,5 +201,103 @@ namespace Avalonia
(_deferredSetter = new DeferredSetter<object>());
}
}
private bool TryGetValue(AvaloniaProperty property, out object value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
{
value = null;
return false;
}
value = _entries[index].Value;
return true;
}
private void AddValueInternal(AvaloniaProperty property, object value)
{
Entry[] entries = new Entry[_entries.Length + 1];
for (int i = 0; i < _entries.Length; ++i)
{
if (_entries[i].PropertyId > property.Id)
{
if (i > 0)
{
Array.Copy(_entries, 0, entries, 0, i);
}
entries[i] = new Entry { PropertyId = property.Id, Value = value };
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
break;
}
}
_entries = entries;
}
private void SetValueInternal(AvaloniaProperty property, object value)
{
_entries[TryFindEntry(property.Id).Item1].Value = value;
}
private (int, bool) TryFindEntry(int propertyId)
{
if (_entries.Length <= 12)
{
// For small lists, we use an optimized linear search. Since the last item in the list
// is always int.MaxValue, we can skip a conditional branch in each iteration.
// By unrolling the loop, we can skip another unconditional branch in each iteration.
if (_entries[0].PropertyId >= propertyId) return (0, _entries[0].PropertyId == propertyId);
if (_entries[1].PropertyId >= propertyId) return (1, _entries[1].PropertyId == propertyId);
if (_entries[2].PropertyId >= propertyId) return (2, _entries[2].PropertyId == propertyId);
if (_entries[3].PropertyId >= propertyId) return (3, _entries[3].PropertyId == propertyId);
if (_entries[4].PropertyId >= propertyId) return (4, _entries[4].PropertyId == propertyId);
if (_entries[5].PropertyId >= propertyId) return (5, _entries[5].PropertyId == propertyId);
if (_entries[6].PropertyId >= propertyId) return (6, _entries[6].PropertyId == propertyId);
if (_entries[7].PropertyId >= propertyId) return (7, _entries[7].PropertyId == propertyId);
if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId);
if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId);
if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId);
}
else
{
int low = 0;
int high = _entries.Length;
int id;
while (high - low > 3)
{
int pivot = (high + low) / 2;
id = _entries[pivot].PropertyId;
if (propertyId == id)
return (pivot, true);
if (propertyId <= id)
high = pivot;
else
low = pivot + 1;
}
do
{
id = _entries[low].PropertyId;
if (id == propertyId)
return (low, true);
if (id > propertyId)
break;
++low;
}
while (low < high);
}
return (0, false);
}
}
}

2
src/Avalonia.Controls/AutoCompleteBox.cs

@ -1893,7 +1893,7 @@ namespace Avalonia.Controls
{
bool callTextChanged = false;
// Update the Text dependency property
if ((userInitiated == null || userInitiated == true) && Text != value)
if ((userInitiated ?? true) && Text != value)
{
_ignoreTextPropertyChange++;
Text = value;

2
src/Avalonia.Controls/Button.cs

@ -217,7 +217,7 @@ namespace Avalonia.Controls
var e = new RoutedEventArgs(ClickEvent);
RaiseEvent(e);
if (Command != null)
if (!e.Handled && Command?.CanExecute(CommandParameter) == true)
{
Command.Execute(CommandParameter);
e.Handled = true;

1
src/Avalonia.Controls/Control.cs

@ -1,6 +1,7 @@
// 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.ComponentModel;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;

76
src/Avalonia.Controls/DropDown.cs

@ -56,6 +56,7 @@ namespace Avalonia.Controls
private bool _isDropDownOpen;
private Popup _popup;
private object _selectionBoxItem;
private IDisposable _subscriptionsOnOpen;
/// <summary>
/// Initializes static members of the <see cref="DropDown"/> class.
@ -149,16 +150,12 @@ namespace Avalonia.Controls
{
if (e.Key == Key.Down)
{
if (++SelectedIndex >= ItemCount)
SelectedIndex = 0;
SelectNext();
e.Handled = true;
}
else if (e.Key == Key.Up)
{
if (--SelectedIndex < 0)
SelectedIndex = ItemCount - 1;
SelectPrev();
e.Handled = true;
}
}
@ -174,6 +171,32 @@ namespace Avalonia.Controls
}
}
/// <inheritdoc/>
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
base.OnPointerWheelChanged(e);
if (!e.Handled)
{
if (!IsDropDownOpen)
{
if (IsFocused)
{
if (e.Delta.Y < 0)
SelectNext();
else
SelectPrev();
e.Handled = true;
}
}
else
{
e.Handled = true;
}
}
}
/// <inheritdoc/>
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
@ -223,6 +246,9 @@ namespace Avalonia.Controls
private void PopupClosed(object sender, EventArgs e)
{
_subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null;
if (CanFocus(this))
{
Focus();
@ -232,6 +258,22 @@ namespace Avalonia.Controls
private void PopupOpened(object sender, EventArgs e)
{
TryFocusSelectedItem();
_subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null;
var toplevel = this.GetVisualRoot() as TopLevel;
if (toplevel != null)
{
_subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) =>
{
//eat wheel scroll event outside dropdown popup while it's open
if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel)
{
ev.Handled = true;
}
}, Interactivity.RoutingStrategies.Tunnel);
}
}
private void SelectedItemChanged(AvaloniaPropertyChangedEventArgs e)
@ -247,7 +289,7 @@ namespace Avalonia.Controls
{
var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
if(container == null && SelectedItems.Count > 0)
if (container == null && SelectedItems.Count > 0)
{
ScrollIntoView(SelectedItems[0]);
container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
@ -307,5 +349,25 @@ namespace Avalonia.Controls
}
}
}
private void SelectNext()
{
int next = SelectedIndex + 1;
if (next >= ItemCount)
next = 0;
SelectedIndex = next;
}
private void SelectPrev()
{
int prev = SelectedIndex - 1;
if (prev < 0)
prev = ItemCount - 1;
SelectedIndex = prev;
}
}
}

1
src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs

@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Platform;

1
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs

@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using Avalonia.Styling;
namespace Avalonia.Controls.Embedding.Offscreen

17
src/Avalonia.Controls/Image.cs

@ -99,5 +99,22 @@ namespace Avalonia.Controls
return new Size();
}
}
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
var source = Source;
if (source != null)
{
var sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height);
var result = Stretch.CalculateSize(finalSize, sourceSize);
return result;
}
else
{
return new Size();
}
}
}
}

2
src/Avalonia.Controls/ItemsControl.cs

@ -64,6 +64,7 @@ namespace Avalonia.Controls
static ItemsControl()
{
ItemsProperty.Changed.AddClassHandler<ItemsControl>(x => x.ItemsChanged);
ItemTemplateProperty.Changed.AddClassHandler<ItemsControl>(x => x.ItemTemplateChanged);
}
/// <summary>
@ -73,7 +74,6 @@ namespace Avalonia.Controls
{
PseudoClasses.Add(":empty");
SubscribeToItems(_items);
ItemTemplateProperty.Changed.AddClassHandler<ItemsControl>(x => x.ItemTemplateChanged);
}
/// <summary>

4
src/Avalonia.Controls/MenuItem.cs

@ -287,7 +287,7 @@ namespace Avalonia.Controls
/// <param name="e">The click event args.</param>
protected virtual void OnClick(RoutedEventArgs e)
{
if (Command != null)
if (!e.Handled && Command?.CanExecute(CommandParameter) == true)
{
Command.Execute(CommandParameter);
e.Handled = true;
@ -421,7 +421,7 @@ namespace Avalonia.Controls
}
/// <summary>
/// Called when the <see cref="Header"/> property changes.
/// Called when the <see cref="HeaderedSelectingItemsControl.Header"/> property changes.
/// </summary>
/// <param name="e">The property change event.</param>
private void HeaderChanged(AvaloniaPropertyChangedEventArgs e)

2
src/Avalonia.Controls/PixelPointEventArgs.cs

@ -13,7 +13,7 @@ namespace Avalonia.Controls
/// <summary>
/// Initializes a new instance of the <see cref="PixelPointEventArgs"/> class.
/// </summary>
/// <param name="point">The <see cref=PixelPoint"/> data.</param>
/// <param name="point">The <see cref="PixelPoint"/> data.</param>
public PixelPointEventArgs(PixelPoint point)
{
Point = point;

2
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -285,7 +285,7 @@ namespace Avalonia.Controls.Presenters
{
scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable);
if (scrollable.IsLogicalScrollEnabled == true)
if (scrollable.IsLogicalScrollEnabled)
{
_logicalScrollSubscription = new CompositeDisposable(
this.GetObservable(CanHorizontallyScrollProperty)

13
src/Avalonia.Controls/Shapes/Shape.cs

@ -20,7 +20,10 @@ namespace Avalonia.Controls.Shapes
AvaloniaProperty.Register<Shape, IBrush>(nameof(Stroke));
public static readonly StyledProperty<AvaloniaList<double>> StrokeDashArrayProperty =
AvaloniaProperty.Register<Shape, AvaloniaList<double>>("StrokeDashArray");
AvaloniaProperty.Register<Shape, AvaloniaList<double>>(nameof(StrokeDashArray));
public static readonly StyledProperty<double> StrokeDashOffsetProperty =
AvaloniaProperty.Register<Shape, double>(nameof(StrokeDashOffset));
public static readonly StyledProperty<double> StrokeThicknessProperty =
AvaloniaProperty.Register<Shape, double>(nameof(StrokeThickness));
@ -103,6 +106,12 @@ namespace Avalonia.Controls.Shapes
get { return GetValue(StrokeDashArrayProperty); }
set { SetValue(StrokeDashArrayProperty, value); }
}
public double StrokeDashOffset
{
get { return GetValue(StrokeDashOffsetProperty); }
set { SetValue(StrokeDashOffsetProperty, value); }
}
public double StrokeThickness
{
@ -124,7 +133,7 @@ namespace Avalonia.Controls.Shapes
if (geometry != null)
{
var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray),
var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray, StrokeDashOffset),
StrokeDashCap, StrokeStartLineCap, StrokeEndLineCap, StrokeJoin);
context.DrawGeometry(Fill, pen, geometry);
}

11
src/Avalonia.Controls/TopLevel.cs

@ -136,6 +136,11 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Fired when the window is opened.
/// </summary>
public event EventHandler Opened;
/// <summary>
/// Fired when the window is closed.
/// </summary>
@ -311,6 +316,12 @@ namespace Avalonia.Controls
$"Control '{GetType().Name}' is a top level control and cannot be added as a child.");
}
/// <summary>
/// Raises the <see cref="Opened"/> event.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnOpened(EventArgs e) => Opened?.Invoke(this, e);
/// <summary>
/// Tries to get a service from an <see cref="IAvaloniaDependencyResolver"/>, logging a
/// warning if not found.

2
src/Avalonia.Controls/Window.cs

@ -389,6 +389,7 @@ namespace Avalonia.Controls
Renderer?.Start();
}
SetWindowStartupLocation(Owner?.PlatformImpl);
OnOpened(EventArgs.Empty);
}
/// <summary>
@ -458,6 +459,7 @@ namespace Avalonia.Controls
owner.Activate();
result.SetResult((TResult)(_dialogResult ?? default(TResult)));
});
OnOpened(EventArgs.Empty);
}
SetWindowStartupLocation(owner);

4
src/Avalonia.Controls/WindowBase.cs

@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
@ -163,7 +164,7 @@ namespace Avalonia.Controls
}
/// <summary>
/// Shows the popup.
/// Shows the window.
/// </summary>
public virtual void Show()
{
@ -181,6 +182,7 @@ namespace Avalonia.Controls
}
PlatformImpl?.Show();
Renderer?.Start();
OnOpened(EventArgs.Empty);
}
finally
{

2
src/Avalonia.Native/ScreenImpl.cs

@ -41,7 +41,7 @@ namespace Avalonia.Native
public void Dispose ()
{
_native.Dispose();
_native?.Dispose();
_native = null;
}
}

6
src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs

@ -29,8 +29,8 @@ namespace Avalonia
{
var windowLoaded = Observable
.FromEventPattern(
x => window.Initialized += x,
x => window.Initialized -= x)
x => window.Opened += x,
x => window.Opened -= x)
.Select(args => true);
var windowUnloaded = Observable
.FromEventPattern(
@ -59,4 +59,4 @@ namespace Avalonia
.DistinctUntilChanged();
}
}
}
}

4
src/Avalonia.ReactiveUI/ReactiveUserControl.cs

@ -16,7 +16,7 @@ namespace Avalonia
public class ReactiveUserControl<TViewModel> : UserControl, IViewFor<TViewModel> where TViewModel : class
{
public static readonly AvaloniaProperty<TViewModel> ViewModelProperty = AvaloniaProperty
.Register<ReactiveWindow<TViewModel>, TViewModel>(nameof(ViewModel));
.Register<ReactiveUserControl<TViewModel>, TViewModel>(nameof(ViewModel));
/// <summary>
/// Initializes a new instance of the <see cref="ReactiveUserControl{TViewModel}"/> class.
@ -41,4 +41,4 @@ namespace Avalonia
set => ViewModel = (TViewModel)value;
}
}
}
}

224
src/Avalonia.ReactiveUI/RoutedViewHost.cs

@ -0,0 +1,224 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Styling;
using ReactiveUI;
using Splat;
namespace Avalonia
{
/// <summary>
/// This control hosts the View associated with ReactiveUI RoutingState,
/// and will display the View and wire up the ViewModel whenever a new
/// ViewModel is navigated to. Nested routing is also supported.
/// </summary>
/// <remarks>
/// <para>
/// ReactiveUI routing consists of an IScreen that contains current
/// RoutingState, several IRoutableViewModels, and a platform-specific
/// XAML control called RoutedViewHost.
/// </para>
/// <para>
/// RoutingState manages the ViewModel navigation stack and allows
/// ViewModels to navigate to other ViewModels. IScreen is the root of
/// a navigation stack; despite the name, its views don't have to occupy
/// the whole screen. RoutedViewHost monitors an instance of RoutingState,
/// responding to any changes in the navigation stack by creating and
/// embedding the appropriate view.
/// </para>
/// <para>
/// Place this control to a view containing your ViewModel that implements
/// IScreen, and bind IScreen.Router property to RoutedViewHost.Router property.
/// <code>
/// <![CDATA[
/// <rxui:RoutedViewHost
/// HorizontalAlignment="Stretch"
/// VerticalAlignment="Stretch"
/// Router="{Binding Router}">
/// <rxui:RoutedViewHost.DefaultContent>
/// <TextBlock Text="Default Content"/>
/// </rxui:RoutedViewHost.DefaultContent>
/// </rxui:RoutedViewHost>
/// ]]>
/// </code>
/// </para>
/// <para>
/// See <see href="https://reactiveui.net/docs/handbook/routing/">
/// ReactiveUI routing documentation website</see> for more info.
/// </para>
/// </remarks>
public class RoutedViewHost : UserControl, IActivatable, IEnableLogger
{
/// <summary>
/// The router dependency property.
/// </summary>
public static readonly AvaloniaProperty<RoutingState> RouterProperty =
AvaloniaProperty.Register<RoutedViewHost, RoutingState>(nameof(Router));
/// <summary>
/// The default content property.
/// </summary>
public static readonly AvaloniaProperty<object> DefaultContentProperty =
AvaloniaProperty.Register<RoutedViewHost, object>(nameof(DefaultContent));
/// <summary>
/// Fade in animation property.
/// </summary>
public static readonly AvaloniaProperty<IAnimation> FadeInAnimationProperty =
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent),
CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25)));
/// <summary>
/// Fade out animation property.
/// </summary>
public static readonly AvaloniaProperty<IAnimation> FadeOutAnimationProperty =
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent),
CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25)));
/// <summary>
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
/// </summary>
public RoutedViewHost()
{
this.WhenActivated(disposables =>
{
this.WhenAnyObservable(x => x.Router.CurrentViewModel)
.DistinctUntilChanged()
.Subscribe(NavigateToViewModel)
.DisposeWith(disposables);
});
}
/// <summary>
/// Gets or sets the <see cref="RoutingState"/> of the view model stack.
/// </summary>
public RoutingState Router
{
get => GetValue(RouterProperty);
set => SetValue(RouterProperty, value);
}
/// <summary>
/// Gets or sets the content displayed whenever there is no page currently routed.
/// </summary>
public object DefaultContent
{
get => GetValue(DefaultContentProperty);
set => SetValue(DefaultContentProperty, value);
}
/// <summary>
/// Gets or sets the animation played when page appears.
/// </summary>
public IAnimation FadeInAnimation
{
get => GetValue(FadeInAnimationProperty);
set => SetValue(FadeInAnimationProperty, value);
}
/// <summary>
/// Gets or sets the animation played when page disappears.
/// </summary>
public IAnimation FadeOutAnimation
{
get => GetValue(FadeOutAnimationProperty);
set => SetValue(FadeOutAnimationProperty, value);
}
/// <summary>
/// Duplicates the Content property with a private setter.
/// </summary>
public new object Content
{
get => base.Content;
private set => base.Content = value;
}
/// <summary>
/// Gets or sets the ReactiveUI view locator used by this router.
/// </summary>
public IViewLocator ViewLocator { get; set; }
/// <summary>
/// Invoked when ReactiveUI router navigates to a view model.
/// </summary>
/// <param name="viewModel">ViewModel to which the user navigates.</param>
/// <exception cref="Exception">
/// Thrown when ViewLocator is unable to find the appropriate view.
/// </exception>
private void NavigateToViewModel(IRoutableViewModel viewModel)
{
if (viewModel == null)
{
this.Log().Info("ViewModel is null, falling back to default content.");
UpdateContent(DefaultContent);
return;
}
var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
var view = viewLocator.ResolveView(viewModel);
if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?");
this.Log().Info($"Ready to show {view} with autowired {viewModel}.");
view.ViewModel = viewModel;
UpdateContent(view);
}
/// <summary>
/// Updates the content with transitions.
/// </summary>
/// <param name="newContent">New content to set.</param>
private async void UpdateContent(object newContent)
{
if (FadeOutAnimation != null)
await FadeOutAnimation.RunAsync(this, Clock);
Content = newContent;
if (FadeInAnimation != null)
await FadeInAnimation.RunAsync(this, Clock);
}
/// <summary>
/// Creates opacity animation for this routed view host.
/// </summary>
/// <param name="from">Opacity to start from.</param>
/// <param name="to">Opacity to finish with.</param>
/// <param name="duration">Duration of the animation.</param>
/// <returns>Animation object instance.</returns>
private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration)
{
return new Avalonia.Animation.Animation
{
Duration = duration,
Children =
{
new KeyFrame
{
Setters =
{
new Setter
{
Property = OpacityProperty,
Value = from
}
},
Cue = new Cue(0d)
},
new KeyFrame
{
Setters =
{
new Setter
{
Property = OpacityProperty,
Value = to
}
},
Cue = new Cue(1d)
}
}
};
}
}
}

7
src/Avalonia.Remote.Protocol/MetsysBson.cs

@ -1190,10 +1190,6 @@ namespace Metsys.Bson
object container = null;
var property = typeHelper.FindProperty(name);
var propertyType = property != null ? property.Type : _typeMap.ContainsKey(storageType) ? _typeMap[storageType] : typeof(object);
if (property == null && typeHelper.Expando == null)
{
throw new BsonException(string.Format("Deserialization failed: type {0} does not have a property named {1}", type.FullName, name));
}
if (property != null && property.Setter == null)
{
container = property.Getter(instance);
@ -1201,7 +1197,8 @@ namespace Metsys.Bson
var value = isNull ? null : DeserializeValue(propertyType, storageType, container, options);
if (property == null)
{
((IDictionary<string, object>)typeHelper.Expando.Getter(instance))[name] = value;
if (typeHelper.Expando != null)
((IDictionary<string, object>)typeHelper.Expando.Getter(instance))[name] = value;
}
else if (container == null && value != null && !property.Ignored)
{

6
src/Avalonia.Remote.Protocol/TcpTransportBase.cs

@ -46,7 +46,7 @@ namespace Avalonia.Remote.Protocol
{
try
{
var cl = await server.AcceptTcpClientAsync();
var cl = await server.AcceptTcpClientAsync().ConfigureAwait(false);
AcceptNew();
await Task.Run(async () =>
{
@ -54,7 +54,7 @@ namespace Avalonia.Remote.Protocol
var t = CreateTransport(_resolver, cl.GetStream(), () => tcs.TrySetResult(0));
cb(t);
await tcs.Task;
});
}).ConfigureAwait(false);
}
catch
{
@ -69,7 +69,7 @@ namespace Avalonia.Remote.Protocol
public async Task<IAvaloniaRemoteTransportConnection> Connect(IPAddress address, int port)
{
var c = new TcpClient();
await c.ConnectAsync(address, port);
await c.ConnectAsync(address, port).ConfigureAwait(false);
return CreateTransport(_resolver, c.GetStream(), ((IDisposable)c).Dispose);
}
}

7
src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs

@ -64,7 +64,7 @@ namespace Avalonia.Remote.Protocol
public Task Send(object data)
{
var tcs = new TaskCompletionSource<int>();
var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
lock (_lock)
{
if (!_workerIsAlive)
@ -79,8 +79,9 @@ namespace Avalonia.Remote.Protocol
});
if (_signal != null)
{
_signal.SetResult(0);
var signal = _signal;
_signal = null;
signal.SetResult(0);
}
}
return tcs.Task;
@ -98,4 +99,4 @@ namespace Avalonia.Remote.Protocol
remove => _onException.Remove(value);
}
}
}
}

72
src/Avalonia.Styling/Styling/NotSelector.cs

@ -0,0 +1,72 @@
// 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.Linq;
namespace Avalonia.Styling
{
/// <summary>
/// The `:not()` style selector.
/// </summary>
internal class NotSelector : Selector
{
private readonly Selector _previous;
private readonly Selector _argument;
private string _selectorString;
/// <summary>
/// Initializes a new instance of the <see cref="NotSelector"/> class.
/// </summary>
/// <param name="previous">The previous selector.</param>
/// <param name="argument">The selector to be not-ed.</param>
public NotSelector(Selector previous, Selector argument)
{
_previous = previous;
_argument = argument ?? throw new InvalidOperationException("Not selector must have a selector argument.");
}
/// <inheritdoc/>
public override bool InTemplate => _argument.InTemplate;
/// <inheritdoc/>
public override bool IsCombinator => false;
/// <inheritdoc/>
public override Type TargetType => _previous?.TargetType;
/// <inheritdoc/>
public override string ToString()
{
if (_selectorString == null)
{
_selectorString = ":not(" + _argument.ToString() + ")";
}
return _selectorString;
}
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
{
var innerResult = _argument.Match(control, subscribe);
switch (innerResult.Result)
{
case SelectorMatchResult.AlwaysThisInstance:
return SelectorMatch.NeverThisInstance;
case SelectorMatchResult.AlwaysThisType:
return SelectorMatch.NeverThisType;
case SelectorMatchResult.NeverThisInstance:
return SelectorMatch.AlwaysThisInstance;
case SelectorMatchResult.NeverThisType:
return SelectorMatch.AlwaysThisType;
case SelectorMatchResult.Sometimes:
return new SelectorMatch(innerResult.Activator.Select(x => !x));
default:
throw new InvalidOperationException("Invalid SelectorMatchResult.");
}
}
protected override Selector MovePrevious() => _previous;
}
}

11
src/Avalonia.Styling/Styling/Selectors.cs

@ -94,6 +94,17 @@ namespace Avalonia.Styling
}
}
/// <summary>
/// Returns a selector which inverts the results of selector argument.
/// </summary>
/// <param name="previous">The previous selector.</param>
/// <param name="argument">The selector to be not-ed.</param>
/// <returns>The selector.</returns>
public static Selector Not(this Selector previous, Func<Selector, Selector> argument)
{
return new NotSelector(previous, argument(null));
}
/// <summary>
/// Returns a selector which matches a type.
/// </summary>

6
src/Avalonia.Styling/Styling/Style.cs

@ -143,6 +143,7 @@ namespace Avalonia.Styling
}
controlSubscriptions.Add(subs);
controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs)));
Subscriptions.Add(subs);
}
@ -159,8 +160,9 @@ namespace Avalonia.Styling
var sub = setter.Apply(this, control, null);
subs.Add(sub);
}
controlSubscriptions.Add(subs);
controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs)));
Subscriptions.Add(subs);
return true;
}
@ -223,7 +225,7 @@ namespace Avalonia.Styling
{
if (!_applied.TryGetValue(control, out var subscriptions))
{
subscriptions = new CompositeDisposable(2);
subscriptions = new CompositeDisposable(3);
subscriptions.Add(control.StyleDetach.Subscribe(ControlDetach));
_applied.Add(control, subscriptions);
}

18
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -255,15 +255,19 @@ namespace Avalonia.Rendering
}
var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(GetContext);
using (scene)
{
var overlay = DrawDirtyRects || DrawFps;
if (DrawDirtyRects)
_dirtyRectsDisplay.Tick();
if (overlay)
RenderOverlay(scene.Item, GetContext());
if (updated || forceComposite || overlay)
RenderComposite(scene.Item, GetContext());
if (scene?.Item != null)
{
var overlay = DrawDirtyRects || DrawFps;
if (DrawDirtyRects)
_dirtyRectsDisplay.Tick();
if (overlay)
RenderOverlay(scene.Item, GetContext());
if (updated || forceComposite || overlay)
RenderComposite(scene.Item, GetContext());
}
}
}
finally

2
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@ -236,7 +236,7 @@ namespace Avalonia.Rendering.SceneGraph
{
foreach (var operation in DrawOperations)
{
if (operation.Item.HitTest(p) == true)
if (operation.Item.HitTest(p))
{
return true;
}

1
src/Avalonia.X11/Avalonia.X11.csproj

@ -7,7 +7,6 @@
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\Gtk\Avalonia.Gtk3\Avalonia.Gtk3.csproj" />
<ProjectReference Include="..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>

263
src/Avalonia.X11/NativeDialogs/Gtk.cs

@ -0,0 +1,263 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Platform.Interop;
// ReSharper disable IdentifierTypo
namespace Avalonia.X11.NativeDialogs
{
static unsafe class Glib
{
private const string GlibName = "libglib-2.0.so.0";
private const string GObjectName = "libgobject-2.0.so.0";
[DllImport(GlibName)]
public static extern void g_slist_free(GSList* data);
[DllImport(GObjectName)]
private static extern void g_object_ref(IntPtr instance);
[DllImport(GObjectName)]
private static extern ulong g_signal_connect_object(IntPtr instance, Utf8Buffer signal,
IntPtr handler, IntPtr userData, int flags);
[DllImport(GObjectName)]
private static extern void g_object_unref(IntPtr instance);
[DllImport(GObjectName)]
private static extern ulong g_signal_handler_disconnect(IntPtr instance, ulong connectionId);
private delegate bool timeout_callback(IntPtr data);
[DllImport(GlibName)]
private static extern ulong g_timeout_add_full(int prio, uint interval, timeout_callback callback, IntPtr data,
IntPtr destroy);
class ConnectedSignal : IDisposable
{
private readonly IntPtr _instance;
private GCHandle _handle;
private readonly ulong _id;
public ConnectedSignal(IntPtr instance, GCHandle handle, ulong id)
{
_instance = instance;
g_object_ref(instance);
_handle = handle;
_id = id;
}
public void Dispose()
{
if (_handle.IsAllocated)
{
g_signal_handler_disconnect(_instance, _id);
g_object_unref(_instance);
_handle.Free();
}
}
}
public static IDisposable ConnectSignal<T>(IntPtr obj, string name, T handler)
{
var handle = GCHandle.Alloc(handler);
var ptr = Marshal.GetFunctionPointerForDelegate((Delegate)(object)handler);
using (var utf = new Utf8Buffer(name))
{
var id = g_signal_connect_object(obj, utf, ptr, IntPtr.Zero, 0);
if (id == 0)
throw new ArgumentException("Unable to connect to signal " + name);
return new ConnectedSignal(obj, handle, id);
}
}
static bool TimeoutHandler(IntPtr data)
{
var handle = GCHandle.FromIntPtr(data);
var cb = (Func<bool>)handle.Target;
if (!cb())
{
handle.Free();
return false;
}
return true;
}
private static readonly timeout_callback s_pinnedHandler;
static Glib()
{
s_pinnedHandler = TimeoutHandler;
}
static void AddTimeout(int priority, uint interval, Func<bool> callback)
{
var handle = GCHandle.Alloc(callback);
g_timeout_add_full(priority, interval, s_pinnedHandler, GCHandle.ToIntPtr(handle), IntPtr.Zero);
}
public static Task<T> RunOnGlibThread<T>(Func<T> action)
{
var tcs = new TaskCompletionSource<T>();
AddTimeout(0, 0, () =>
{
try
{
tcs.SetResult(action());
}
catch (Exception e)
{
tcs.TrySetException(e);
}
return false;
});
return tcs.Task;
}
}
[StructLayout(LayoutKind.Sequential)]
unsafe struct GSList
{
public readonly IntPtr Data;
public readonly GSList* Next;
}
enum GtkFileChooserAction
{
Open,
Save,
SelectFolder,
}
// ReSharper disable UnusedMember.Global
enum GtkResponseType
{
Help = -11,
Apply = -10,
No = -9,
Yes = -8,
Close = -7,
Cancel = -6,
Ok = -5,
DeleteEvent = -4,
Accept = -3,
Reject = -2,
None = -1,
}
// ReSharper restore UnusedMember.Global
static unsafe class Gtk
{
private static IntPtr s_display;
private const string GdkName = "libgdk-3.so.0";
private const string GtkName = "libgtk-3.so.0";
[DllImport(GtkName)]
static extern void gtk_main_iteration();
[DllImport(GtkName)]
public static extern void gtk_window_set_modal(IntPtr window, bool modal);
[DllImport(GtkName)]
public static extern void gtk_window_present(IntPtr gtkWindow);
public delegate bool signal_generic(IntPtr gtkWidget, IntPtr userData);
public delegate bool signal_dialog_response(IntPtr gtkWidget, GtkResponseType response, IntPtr userData);
[DllImport(GtkName)]
public static extern IntPtr gtk_file_chooser_dialog_new(Utf8Buffer title, IntPtr parent,
GtkFileChooserAction action, IntPtr ignore);
[DllImport(GtkName)]
public static extern void gtk_file_chooser_set_select_multiple(IntPtr chooser, bool allow);
[DllImport(GtkName)]
public static extern void
gtk_dialog_add_button(IntPtr raw, Utf8Buffer button_text, GtkResponseType response_id);
[DllImport(GtkName)]
public static extern GSList* gtk_file_chooser_get_filenames(IntPtr chooser);
[DllImport(GtkName)]
public static extern void gtk_file_chooser_set_filename(IntPtr chooser, Utf8Buffer file);
[DllImport(GtkName)]
public static extern void gtk_widget_realize(IntPtr gtkWidget);
[DllImport(GtkName)]
public static extern IntPtr gtk_widget_get_window(IntPtr gtkWidget);
[DllImport(GtkName)]
public static extern void gtk_widget_hide(IntPtr gtkWidget);
[DllImport(GtkName)]
static extern bool gtk_init_check(int argc, IntPtr argv);
[DllImport(GdkName)]
static extern IntPtr gdk_x11_window_foreign_new_for_display(IntPtr display, IntPtr xid);
[DllImport(GdkName)]
static extern IntPtr gdk_set_allowed_backends(Utf8Buffer backends);
[DllImport(GdkName)]
static extern IntPtr gdk_display_get_default();
[DllImport(GtkName)]
static extern IntPtr gtk_application_new(Utf8Buffer appId, int flags);
[DllImport(GdkName)]
public static extern void gdk_window_set_transient_for(IntPtr window, IntPtr parent);
public static IntPtr GetForeignWindow(IntPtr xid) => gdk_x11_window_foreign_new_for_display(s_display, xid);
public static Task<bool> StartGtk()
{
var tcs = new TaskCompletionSource<bool>();
new Thread(() =>
{
try
{
using (var backends = new Utf8Buffer("x11"))
gdk_set_allowed_backends(backends);
}
catch
{
//Ignore
}
Environment.SetEnvironmentVariable("WAYLAND_DISPLAY",
"/proc/fake-display-to-prevent-wayland-initialization-by-gtk3");
if (!gtk_init_check(0, IntPtr.Zero))
{
tcs.SetResult(false);
return;
}
IntPtr app;
using (var utf = new Utf8Buffer($"avalonia.app.a{Guid.NewGuid():N}"))
app = gtk_application_new(utf, 0);
if (app == IntPtr.Zero)
{
tcs.SetResult(false);
return;
}
s_display = gdk_display_get_default();
tcs.SetResult(true);
while (true)
gtk_main_iteration();
}) {Name = "GTK3THREAD", IsBackground = true}.Start();
return tcs.Task;
}
}
}

122
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Avalonia.Platform.Interop;
using static Avalonia.X11.NativeDialogs.Glib;
using static Avalonia.X11.NativeDialogs.Gtk;
// ReSharper disable AccessToModifiedClosure
namespace Avalonia.X11.NativeDialogs
{
class GtkSystemDialog : ISystemDialogImpl
{
private Task<bool> _initialized;
private unsafe Task<string[]> ShowDialog(string title, IWindowImpl parent, GtkFileChooserAction action,
bool multiSelect, string initialFileName)
{
IntPtr dlg;
using (var name = new Utf8Buffer(title))
dlg = gtk_file_chooser_dialog_new(name, IntPtr.Zero, action, IntPtr.Zero);
UpdateParent(dlg, parent);
if (multiSelect)
gtk_file_chooser_set_select_multiple(dlg, true);
gtk_window_set_modal(dlg, true);
var tcs = new TaskCompletionSource<string[]>();
List<IDisposable> disposables = null;
void Dispose()
{
// ReSharper disable once PossibleNullReferenceException
foreach (var d in disposables) d.Dispose();
disposables.Clear();
}
disposables = new List<IDisposable>
{
ConnectSignal<signal_generic>(dlg, "close", delegate
{
tcs.TrySetResult(null);
Dispose();
return false;
}),
ConnectSignal<signal_dialog_response>(dlg, "response", (_, resp, __) =>
{
string[] result = null;
if (resp == GtkResponseType.Accept)
{
var resultList = new List<string>();
var gs = gtk_file_chooser_get_filenames(dlg);
var cgs = gs;
while (cgs != null)
{
if (cgs->Data != IntPtr.Zero)
resultList.Add(Utf8Buffer.StringFromPtr(cgs->Data));
cgs = cgs->Next;
}
g_slist_free(gs);
result = resultList.ToArray();
}
gtk_widget_hide(dlg);
Dispose();
tcs.TrySetResult(result);
return false;
})
};
using (var open = new Utf8Buffer("Open"))
gtk_dialog_add_button(dlg, open, GtkResponseType.Accept);
using (var open = new Utf8Buffer("Cancel"))
gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel);
if (initialFileName != null)
using (var fn = new Utf8Buffer(initialFileName))
gtk_file_chooser_set_filename(dlg, fn);
gtk_window_present(dlg);
return tcs.Task;
}
public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
{
await EnsureInitialized();
return await await RunOnGlibThread(
() => ShowDialog(dialog.Title, parent,
dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save,
(dialog as OpenFileDialog)?.AllowMultiple ?? false,
Path.Combine(string.IsNullOrEmpty(dialog.InitialDirectory) ? "" : dialog.InitialDirectory,
string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName)));
}
public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
{
await EnsureInitialized();
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(dialog.Title, parent,
GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory);
return res?.FirstOrDefault();
});
}
async Task EnsureInitialized()
{
if (_initialized == null) _initialized = StartGtk();
if (!(await _initialized))
throw new Exception("Unable to initialize GTK on separate thread");
}
void UpdateParent(IntPtr chooser, IWindowImpl parentWindow)
{
var xid = parentWindow.Handle.Handle;
gtk_widget_realize(chooser);
var window = gtk_widget_get_window(chooser);
var parent = GetForeignWindow(xid);
if (window != IntPtr.Zero && parent != IntPtr.Zero)
gdk_window_set_transient_for(window, parent);
}
}
}

4
src/Avalonia.X11/X11Platform.cs

@ -2,7 +2,6 @@
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Gtk3;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.OpenGL;
@ -10,6 +9,7 @@ using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.X11;
using Avalonia.X11.Glx;
using Avalonia.X11.NativeDialogs;
using static Avalonia.X11.XLib;
namespace Avalonia.X11
{
@ -45,7 +45,7 @@ namespace Avalonia.X11
.Bind<IClipboard>().ToConstant(new X11Clipboard(this))
.Bind<IPlatformSettings>().ToConstant(new PlatformSettingsStub())
.Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader(Info))
.Bind<ISystemDialogImpl>().ToConstant(new Gtk3ForeignX11SystemDialog());
.Bind<ISystemDialogImpl>().ToConstant(new GtkSystemDialog());
X11Screens = Avalonia.X11.X11Screens.Init(this);
Screens = new X11Screens(X11Screens);

3
src/Avalonia.X11/X11Screens.cs

@ -218,6 +218,7 @@ namespace Avalonia.X11
class X11Screen
{
private const int FullHDWidth = 1920;
public bool Primary { get; }
public string Name { get; set; }
public PixelRect Bounds { get; set; }
@ -247,6 +248,6 @@ namespace Avalonia.X11
}
public static double GuessPixelDensity(double pixelWidth, double mmWidth)
=> Math.Max(1, Math.Round(pixelWidth / mmWidth * 25.4 / 96));
=> pixelWidth <= FullHDWidth ? 1 : Math.Max(1, Math.Round(pixelWidth / mmWidth * 25.4 / 96));
}
}

17
src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs

@ -8,11 +8,14 @@ using Avalonia.Platform;
using Portable.Xaml;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Xml.Linq;
using System.Linq;
namespace Avalonia.Markup.Xaml
{
@ -240,15 +243,21 @@ namespace Avalonia.Markup.Xaml
{
using (var xamlInfoStream = assetLocator.Open(xamlInfoUri))
{
var xamlInfo = (AvaloniaResourceXamlInfo)s_xamlInfoSerializer.ReadObject(xamlInfoStream);
if (xamlInfo.ClassToResourcePathIndex.TryGetValue(typeName, out var rv) == true)
var assetDoc = XDocument.Load(xamlInfoStream);
XNamespace assetNs = assetDoc.Root.Attribute("xmlns").Value;
XNamespace arrayNs = "http://schemas.microsoft.com/2003/10/Serialization/Arrays";
Dictionary<string,string> xamlInfo =
assetDoc.Root.Element(assetNs + "ClassToResourcePathIndex").Elements(arrayNs + "KeyValueOfstringstring")
.ToDictionary(entry =>entry.Element(arrayNs + "Key").Value,
entry => entry.Element(arrayNs + "Value").Value);
if (xamlInfo.TryGetValue(typeName, out var rv))
{
yield return new Uri($"avares://{asm}{rv}");
yield break;
}
}
}
}
yield return new Uri("resm:" + typeName + ".xaml?assembly=" + asm);
yield return new Uri("resm:" + typeName + ".paml?assembly=" + asm);

35
src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs

@ -77,40 +77,15 @@ namespace Avalonia.Markup.Xaml.PortableXaml
_delayedValuesHelper.ApplyAll();
}
protected internal override void OnAfterBeginInit(object value)
{
//not called for avalonia objects
//as it's called inly for
//Portable.Xaml.ComponentModel.ISupportInitialize
base.OnAfterBeginInit(value);
}
protected internal override void OnAfterEndInit(object value)
{
//not called for avalonia objects
//as it's called inly for
//Portable.Xaml.ComponentModel.ISupportInitialize
base.OnAfterEndInit(value);
}
protected internal override void OnAfterProperties(object value)
{
_delayedValuesHelper.EndInit(value);
base.OnAfterProperties(value);
//AfterEndInit is not called as it supports only
//Portable.Xaml.ComponentModel.ISupportInitialize
//and we have Avalonia.ISupportInitialize so we need some hacks
HandleEndEdit(value);
}
protected internal override void OnBeforeProperties(object value)
{
//OnAfterBeginInit is not called as it supports only
//Portable.Xaml.ComponentModel.ISupportInitialize
//and we have Avalonia.ISupportInitialize so we need some hacks
HandleBeginInit(value);
if (value != null)
_delayedValuesHelper.BeginInit(value);
@ -127,16 +102,6 @@ namespace Avalonia.Markup.Xaml.PortableXaml
return base.OnSetValue(target, member, value);
}
private void HandleBeginInit(object value)
{
(value as Avalonia.ISupportInitialize)?.BeginInit();
}
private void HandleEndEdit(object value)
{
(value as Avalonia.ISupportInitialize)?.EndInit();
}
public override void WriteStartMember(XamlMember property)
{
foreach(var d in DesignDirectives)

2
src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs

@ -33,7 +33,7 @@ namespace Avalonia.Markup.Xaml.PortableXaml
private IRuntimeTypeProvider _avaloniaTypeProvider;
protected internal override XamlType GetXamlType(string xamlNamespace, string name, params XamlType[] typeArguments)
protected override XamlType GetXamlType(string xamlNamespace, string name, params XamlType[] typeArguments)
{
XamlType type = null;
try

2
src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github

@ -1 +1 @@
Subproject commit 8abbe09592668efb573ac4d5548ba2d7e464ba78
Subproject commit ab5526173722b8988bc5ca3c03c8752ce89c0975

51
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Data.Core;
using Avalonia.Utilities;
@ -32,6 +33,11 @@ namespace Avalonia.Markup.Parsers
public static IEnumerable<ISyntax> Parse(string s)
{
var r = new CharacterReader(s.AsSpan());
return Parse(ref r, null);
}
private static IEnumerable<ISyntax> Parse(ref CharacterReader r, char? end)
{
var state = State.Start;
var selector = new List<ISyntax>();
while (!r.End && state != State.End)
@ -43,7 +49,7 @@ namespace Avalonia.Markup.Parsers
state = ParseStart(ref r);
break;
case State.Middle:
state = ParseMiddle(ref r);
state = ParseMiddle(ref r, end);
break;
case State.CanHaveType:
state = ParseCanHaveType(ref r);
@ -107,7 +113,7 @@ namespace Avalonia.Markup.Parsers
return State.TypeName;
}
private static State ParseMiddle(ref CharacterReader r)
private static State ParseMiddle(ref CharacterReader r, char? end)
{
if (r.TakeIf(':'))
{
@ -129,6 +135,10 @@ namespace Avalonia.Markup.Parsers
{
return State.Name;
}
else if (end.HasValue && !r.End && r.Peek == end.Value)
{
return State.End;
}
return State.TypeName;
}
@ -151,16 +161,23 @@ namespace Avalonia.Markup.Parsers
}
const string IsKeyword = "is";
const string NotKeyword = "not";
if (identifier.SequenceEqual(IsKeyword.AsSpan()) && r.TakeIf('('))
{
var syntax = ParseType(ref r, new IsSyntax());
if (r.End || !r.TakeIf(')'))
{
throw new ExpressionParseException(r.Position, $"Expected ')', got {r.Peek}");
}
Expect(ref r, ')');
return (State.CanHaveType, syntax);
}
if (identifier.SequenceEqual(NotKeyword.AsSpan()) && r.TakeIf('('))
{
var argument = Parse(ref r, ')');
Expect(ref r, ')');
var syntax = new NotSyntax { Argument = argument };
return (State.Middle, syntax);
}
else
{
return (
@ -282,6 +299,18 @@ namespace Avalonia.Markup.Parsers
return syntax;
}
private static void Expect(ref CharacterReader r, char c)
{
if (r.End)
{
throw new ExpressionParseException(r.Position, $"Expected '{c}', got end of selector.");
}
else if (!r.TakeIf(')'))
{
throw new ExpressionParseException(r.Position, $"Expected '{c}', got '{r.Peek}'.");
}
}
public interface ISyntax
{
}
@ -376,5 +405,15 @@ namespace Avalonia.Markup.Parsers
return obj is TemplateSyntax;
}
}
public class NotSyntax : ISyntax
{
public IEnumerable<ISyntax> Argument { get; set; }
public override bool Equals(object obj)
{
return (obj is NotSyntax not) && Argument.SequenceEqual(not.Argument);
}
}
}
}

13
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs

@ -2,6 +2,7 @@
// 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.Globalization;
using Avalonia.Styling;
using Avalonia.Utilities;
@ -25,7 +26,7 @@ namespace Avalonia.Markup.Parsers
/// </param>
public SelectorParser(Func<string, string, Type> typeResolver)
{
this._typeResolver = typeResolver;
_typeResolver = typeResolver;
}
/// <summary>
@ -36,6 +37,11 @@ namespace Avalonia.Markup.Parsers
public Selector Parse(string s)
{
var syntax = SelectorGrammar.Parse(s);
return Create(syntax);
}
private Selector Create(IEnumerable<SelectorGrammar.ISyntax> syntax)
{
var result = default(Selector);
foreach (var i in syntax)
@ -97,6 +103,11 @@ namespace Avalonia.Markup.Parsers
case SelectorGrammar.TemplateSyntax template:
result = result.Template();
break;
case SelectorGrammar.NotSyntax not:
result = result.Not(x => Create(not.Argument));
break;
default:
throw new NotSupportedException($"Unsupported selector grammar '{i.GetType()}'.");
}
}

17
tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs

@ -19,6 +19,19 @@ namespace Avalonia.Base.UnitTests
p = AttachedOwner.AttachedProperty;
}
[Fact]
public void Registered_Properties_Count_Reflects_Newly_Added_Attached_Property()
{
var registry = new AvaloniaPropertyRegistry();
var metadata = new StyledPropertyMetadata<int>();
var property = new AttachedProperty<int>("test", typeof(object), metadata, true);
registry.Register(typeof(object), property);
registry.RegisterAttached(typeof(AvaloniaPropertyRegistryTests), property);
property.AddOwner<Class4>();
Assert.Equal(1, registry.Properties.Count);
}
[Fact]
public void GetRegistered_Returns_Registered_Properties()
{
@ -138,5 +151,9 @@ namespace Avalonia.Base.UnitTests
private class AttachedOwner2 : AttachedOwner
{
}
private class Class4 : AvaloniaObject
{
}
}
}

20
tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs

@ -150,6 +150,26 @@ namespace Avalonia.Base.UnitTests.Data.Core
}
}
[Fact]
public void Should_Work_With_Value_Type()
{
using (var sync = UnitTestSynchronizationContext.Begin())
{
var source = new BehaviorSubject<int>(1);
var data = new { Foo = source };
var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding());
var result = new List<int>();
var sub = target.Subscribe(x => result.Add((int)x));
source.OnNext(42);
sync.ExecutePostedCallbacks();
Assert.Equal(new[] { 1, 42 }, result);
GC.KeepAlive(data);
}
}
private class Class1 : NotifyingBase
{
public Subject<Class2> Next { get; } = new Subject<Class2>();

56
tests/Avalonia.Controls.UnitTests/ImageTests.cs

@ -61,5 +61,61 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Size(50, 50), target.DesiredSize);
}
[Fact]
public void Arrange_Should_Return_Correct_Size_For_No_Stretch()
{
var bitmap = Mock.Of<IBitmap>(x => x.PixelSize == new PixelSize(50, 100));
var target = new Image();
target.Stretch = Stretch.None;
target.Source = bitmap;
target.Measure(new Size(50, 50));
target.Arrange(new Rect(0, 0, 100, 400));
Assert.Equal(new Size(50, 100), target.Bounds.Size);
}
[Fact]
public void Arrange_Should_Return_Correct_Size_For_Fill_Stretch()
{
var bitmap = Mock.Of<IBitmap>(x => x.PixelSize == new PixelSize(50, 100));
var target = new Image();
target.Stretch = Stretch.Fill;
target.Source = bitmap;
target.Measure(new Size(50, 50));
target.Arrange(new Rect(0, 0, 25, 100));
Assert.Equal(new Size(25, 100), target.Bounds.Size);
}
[Fact]
public void Arrange_Should_Return_Correct_Size_For_Uniform_Stretch()
{
var bitmap = Mock.Of<IBitmap>(x => x.PixelSize == new PixelSize(50, 100));
var target = new Image();
target.Stretch = Stretch.Uniform;
target.Source = bitmap;
target.Measure(new Size(50, 50));
target.Arrange(new Rect(0, 0, 25, 100));
Assert.Equal(new Size(25, 50), target.Bounds.Size);
}
[Fact]
public void Arrange_Should_Return_Correct_Size_For_UniformToFill_Stretch()
{
var bitmap = Mock.Of<IBitmap>(x => x.PixelSize == new PixelSize(50, 100));
var target = new Image();
target.Stretch = Stretch.UniformToFill;
target.Source = bitmap;
target.Measure(new Size(50, 50));
target.Arrange(new Rect(0, 0, 25, 100));
Assert.Equal(new Size(25, 100), target.Bounds.Size);
}
}
}

1
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;

16
tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs

@ -199,6 +199,22 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Showing_Should_Raise_Opened()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var target = new TestWindowBase();
var raised = false;
target.Opened += (s, e) => raised = true;
target.Show();
Assert.True(raised);
}
}
[Fact]
public void Hiding_Should_Stop_Renderer()
{

21
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -228,18 +228,35 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void ShowDialog_Should_Start_Renderer()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var parent = Mock.Of<IWindowImpl>();
var renderer = new Mock<IRenderer>();
var target = new Window(CreateImpl(renderer));
target.Show();
target.ShowDialog<object>(parent);
renderer.Verify(x => x.Start(), Times.Once);
}
}
[Fact]
public void ShowDialog_Should_Raise_Opened()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var parent = Mock.Of<IWindowImpl>();
var target = new Window();
var raised = false;
target.Opened += (s, e) => raised = true;
target.ShowDialog<object>(parent);
Assert.True(raised);
}
}
[Fact]
public void Hiding_Should_Stop_Renderer()
{

62
tests/Avalonia.DesignerSupport.Tests/Helpers.cs

@ -0,0 +1,62 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.DesignerSupport.Tests
{
static class Helpers
{
public static void StructDiff(object parsed, object expected) => StructDiff(parsed, expected, "{root}");
static void StructDiff(object parsed, object expected, string path)
{
if (parsed == null && expected == null)
return;
if ((parsed == null && expected != null) || (parsed != null && expected == null))
throw new Exception(
$"{path}: Null mismatch: {(parsed == null ? "null" : "not-null")} {(expected == null ? "null" : "not-null")}");
if (parsed.GetType() != expected.GetType())
throw new Exception($"{path}: Type mismatch: {parsed.GetType()} {expected.GetType()}");
if (parsed is string || parsed.GetType().IsPrimitive)
{
if (!parsed.Equals(expected))
throw new Exception($"{path}: Not equal {parsed} {expected}");
}
else if (parsed is IDictionary dic)
{
var dic2 = (IDictionary) expected;
if (dic.Count != dic2.Count)
throw new Exception($"{path}: Dictionary count mismatch: {dic.Count} {dic2.Count}");
foreach (var k in dic.Keys.Cast<object>().OrderBy(o => o.ToString()))
{
var v1 = dic[k];
var v2 = dic2[k];
StructDiff(v1, v2, path + "['" + k + "']");
}
}
else if (parsed is IList col)
{
var col2 = (IList) expected;
if (col.Count != col2.Count)
throw new Exception($"{path}: Collection count mismatch: {col.Count} {col2.Count}");
for (var c = 0; c < col.Count; c++)
StructDiff(col[c], col2[c], path + "[" + c + "]");
}
else
{
foreach (var prop in parsed.GetType().GetProperties()
.Where(p => p.GetMethod != null && p.GetMethod.IsPublic))
{
StructDiff(prop.GetValue(parsed), prop.GetValue(expected), path + "." + prop.Name);
}
}
}
}
}

171
tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs

@ -0,0 +1,171 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Viewport;
using Xunit;
namespace Avalonia.DesignerSupport.Tests
{
public class RemoteProtocolTests : IDisposable
{
private readonly List<IDisposable> _disposables = new List<IDisposable>();
private IAvaloniaRemoteTransportConnection _server;
private IAvaloniaRemoteTransportConnection _client;
private BlockingCollection<object> _serverMessages = new BlockingCollection<object>();
private BlockingCollection<object> _clientMessages = new BlockingCollection<object>();
private SynchronizationContext _originalContext;
class DisabledSyncContext : SynchronizationContext
{
public override void Post(SendOrPostCallback d, object state)
{
throw new InvalidCastException("Not allowed");
}
public override void Send(SendOrPostCallback d, object state)
{
throw new InvalidCastException("Not allowed");
}
}
void Init(IMessageTypeResolver clientResolver = null, IMessageTypeResolver serverResolver = null)
{
_originalContext = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(new DisabledSyncContext());
var clientTransport = new BsonTcpTransport(clientResolver ?? new DefaultMessageTypeResolver());
var serverTransport = new BsonTcpTransport(serverResolver ?? new DefaultMessageTypeResolver());
var tcpListener = new TcpListener(IPAddress.Loopback, 0);
tcpListener.Start();
var port = ((IPEndPoint)tcpListener.LocalEndpoint).Port;
tcpListener.Stop();
var tcs = new TaskCompletionSource<int>();
serverTransport.Listen(IPAddress.Loopback, port, connected =>
{
_server = connected;
tcs.SetResult(0);
});
_client = clientTransport.Connect(IPAddress.Loopback, port).Result;
_disposables.Add(_client);
_client.OnMessage += (_, m) => _clientMessages.Add(m);
tcs.Task.Wait();
_disposables.Add(_server);
_server.OnMessage += (_, m) => _serverMessages.Add(m);
}
object TakeServer()
{
var src = new CancellationTokenSource(200);
try
{
return _serverMessages.Take(src.Token);
}
finally
{
src.Dispose();
}
}
[Fact]
void EntitiesAreProperlySerializedAndDeserialized()
{
Init();
var rnd = new Random();
_server.OnMessage += (_, message) => { };
object GetRandomValue(Type t, string pathInfo)
{
if (t.IsArray)
{
var arr = Array.CreateInstance(t.GetElementType(), 1);
((IList)arr)[0] = GetRandomValue(t.GetElementType(), pathInfo);
return arr;
}
if (t == typeof(bool))
return true;
if (t == typeof(int) || t == typeof(long))
return rnd.Next();
if (t == typeof(byte))
return (byte)rnd.Next(255);
if (t == typeof(double))
return rnd.NextDouble();
if (t.IsEnum)
return ((IList)Enum.GetValues(t)).Cast<object>().Last();
if (t == typeof(string))
return Guid.NewGuid().ToString();
if (t == typeof(Guid))
return Guid.NewGuid();
throw new Exception($"Doesn't know how to fabricate a random value for {t}, path {pathInfo}");
}
foreach (var t in typeof(MeasureViewportMessage).Assembly.GetTypes().Where(t =>
t.GetCustomAttribute(typeof(AvaloniaRemoteMessageGuidAttribute)) != null))
{
var o = Activator.CreateInstance(t);
foreach (var p in t.GetProperties())
p.SetValue(o, GetRandomValue(p.PropertyType, $"{t.FullName}.{p.Name}"));
_client.Send(o).Wait(200);
var received = TakeServer();
Helpers.StructDiff(received, o);
}
}
[Fact]
void RemoteProtocolShouldBeBackwardsCompatible()
{
Init(new DefaultMessageTypeResolver(typeof(ExtendedMeasureViewportMessage).Assembly));
_client.Send(new ExtendedMeasureViewportMessage()
{
Width = 100, Height = 200, SomeNewProperty = 300,
SomeArrayProperty = new[]{1,2,3},
SubObjectProperty = new ExtendedMeasureViewportMessage.SubObject()
{
Foo = 543
}
});
var received = (MeasureViewportMessage)TakeServer();
Assert.Equal(100, received.Width);
Assert.Equal(200, received.Height);
}
public void Dispose()
{
_disposables.ForEach(d => d.Dispose());
SynchronizationContext.SetSynchronizationContext(_originalContext);
}
}
[AvaloniaRemoteMessageGuid("6E3C5310-E2B1-4C3D-8688-01183AA48C5B")]
public class ExtendedMeasureViewportMessage
{
public double Width { get; set; }
public int SomeNewProperty { get; set; }
public int[] SomeArrayProperty { get; set; }
public class SubObject
{
public int Foo { get; set; }
}
public SubObject SubObjectProperty { get; set; }
public double Height { get; set; }
}
}

73
tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs

@ -200,6 +200,67 @@ namespace Avalonia.Markup.UnitTests.Parsers
result);
}
[Fact]
public void Not_OfType()
{
var result = SelectorGrammar.Parse(":not(Button)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.NotSyntax
{
Argument = new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
},
}
},
result);
}
[Fact]
public void OfType_Not_Class()
{
var result = SelectorGrammar.Parse("Button:not(.foo)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.NotSyntax
{
Argument = new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.ClassSyntax { Class = "foo" },
},
}
},
result);
}
[Fact]
public void Is_Descendent_Not_OfType_Class()
{
var result = SelectorGrammar.Parse(":is(Control) :not(Button.foo)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.IsSyntax { TypeName = "Control" },
new SelectorGrammar.DescendantSyntax { },
new SelectorGrammar.NotSyntax
{
Argument = new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.ClassSyntax { Class = "foo" },
},
}
},
result);
}
[Fact]
public void Namespace_Alone_Fails()
{
@ -223,5 +284,17 @@ namespace Avalonia.Markup.UnitTests.Parsers
{
Assert.Throws<ExpressionParseException>(() => SelectorGrammar.Parse(".%foo"));
}
[Fact]
public void Not_Without_Argument_Fails()
{
Assert.Throws<ExpressionParseException>(() => SelectorGrammar.Parse(":not()"));
}
[Fact]
public void Not_Without_Closing_Parenthesis_Fails()
{
Assert.Throws<ExpressionParseException>(() => SelectorGrammar.Parse(":not(Button"));
}
}
}

3
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs

@ -4,6 +4,7 @@
using Avalonia.Controls;
using Avalonia.LogicalTree;
using System.Collections.Generic;
using System.ComponentModel;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
@ -39,4 +40,4 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Order.Add($"EndInit {InitState}");
}
}
}
}

28
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@ -198,5 +198,33 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
ex.InnerException.Message);
}
}
[Fact]
public void Style_Can_Use_Not_Selector()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='Border:not(.foo)'>
<Setter Property='Background' Value='Red'/>
</Style>
</Window.Styles>
<StackPanel>
<Border Name='foo' Classes='foo bar'/>
<Border Name='notFoo' Classes='bar'/>
</StackPanel>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var foo = window.FindControl<Border>("foo");
var notFoo = window.FindControl<Border>("notFoo");
Assert.Null(foo.Background);
Assert.Equal(Colors.Red, ((ISolidColorBrush)notFoo.Background).Color);
}
}
}
}

35
tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs

@ -10,6 +10,7 @@ using ReactiveUI;
using DynamicData;
using Xunit;
using Splat;
using Avalonia.Markup.Xaml;
namespace Avalonia
{
@ -70,12 +71,40 @@ namespace Avalonia
public class ActivatableWindow : ReactiveWindow<ActivatableViewModel>
{
public ActivatableWindow() => this.WhenActivated(disposables => { });
public ActivatableWindow()
{
InitializeComponent();
Assert.IsType<Border>(Content);
this.WhenActivated(disposables => { });
}
private void InitializeComponent()
{
var loader = new AvaloniaXamlLoader();
loader.Load(@"
<Window xmlns='https://github.com/avaloniaui'>
<Border/>
</Window>", null, this);
}
}
public class ActivatableUserControl : ReactiveUserControl<ActivatableViewModel>
{
public ActivatableUserControl() => this.WhenActivated(disposables => { });
public ActivatableUserControl()
{
InitializeComponent();
Assert.IsType<Border>(Content);
this.WhenActivated(disposables => { });
}
private void InitializeComponent()
{
var loader = new AvaloniaXamlLoader();
loader.Load(@"
<UserControl xmlns='https://github.com/avaloniaui'>
<Border/>
</UserControl>", null, this);
}
}
public AvaloniaActivationForViewFetcherTest()
@ -183,4 +212,4 @@ namespace Avalonia
Assert.False(viewModel.IsActivated);
}
}
}
}

104
tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs

@ -0,0 +1,104 @@
using System;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using Avalonia.Controls;
using Avalonia.Rendering;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Avalonia;
using ReactiveUI;
using DynamicData;
using Xunit;
using Splat;
using Avalonia.Markup.Xaml;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Reactive;
namespace Avalonia
{
public class RoutedViewHostTest
{
public class FirstRoutableViewModel : ReactiveObject, IRoutableViewModel
{
public string UrlPathSegment => "first";
public IScreen HostScreen { get; set; }
}
public class FirstRoutableView : ReactiveUserControl<FirstRoutableViewModel> { }
public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel
{
public string UrlPathSegment => "second";
public IScreen HostScreen { get; set; }
}
public class SecondRoutableView : ReactiveUserControl<SecondRoutableViewModel> { }
public class ScreenViewModel : ReactiveObject, IScreen
{
public RoutingState Router { get; } = new RoutingState();
}
public RoutedViewHostTest()
{
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
Locator.CurrentMutable.Register(() => new FirstRoutableView(), typeof(IViewFor<FirstRoutableViewModel>));
Locator.CurrentMutable.Register(() => new SecondRoutableView(), typeof(IViewFor<SecondRoutableViewModel>));
}
[Fact]
public void RoutedViewHostShouldStayInSyncWithRoutingState()
{
var screen = new ScreenViewModel();
var defaultContent = new TextBlock();
var host = new RoutedViewHost
{
Router = screen.Router,
DefaultContent = defaultContent,
FadeOutAnimation = null,
FadeInAnimation = null
};
var root = new TestRoot
{
Child = host
};
Assert.NotNull(host.Content);
Assert.Equal(typeof(TextBlock), host.Content.GetType());
Assert.Equal(defaultContent, host.Content);
screen.Router.Navigate
.Execute(new FirstRoutableViewModel())
.Subscribe();
Assert.NotNull(host.Content);
Assert.Equal(typeof(FirstRoutableView), host.Content.GetType());
screen.Router.Navigate
.Execute(new SecondRoutableViewModel())
.Subscribe();
Assert.NotNull(host.Content);
Assert.Equal(typeof(SecondRoutableView), host.Content.GetType());
screen.Router.NavigateBack
.Execute(Unit.Default)
.Subscribe();
Assert.NotNull(host.Content);
Assert.Equal(typeof(FirstRoutableView), host.Content.GetType());
screen.Router.NavigateBack
.Execute(Unit.Default)
.Subscribe();
Assert.NotNull(host.Content);
Assert.Equal(typeof(TextBlock), host.Content.GetType());
Assert.Equal(defaultContent, host.Content);
}
}
}

114
tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs

@ -0,0 +1,114 @@
// 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.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Xunit;
namespace Avalonia.Styling.UnitTests
{
public class SelectorTests_Not
{
[Fact]
public void Not_Selector_Should_Have_Correct_String_Representation()
{
var target = default(Selector).Not(x => x.Class("foo"));
Assert.Equal(":not(.foo)", target.ToString());
}
[Fact]
public void Not_OfType_Matches_Control_Of_Incorrect_Type()
{
var control = new Control1();
var target = default(Selector).Not(x => x.OfType<Control1>());
Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(control).Result);
}
[Fact]
public void Not_OfType_Doesnt_Match_Control_Of_Correct_Type()
{
var control = new Control2();
var target = default(Selector).Not(x => x.OfType<Control1>());
Assert.Equal(SelectorMatchResult.AlwaysThisType, target.Match(control).Result);
}
[Fact]
public async Task Not_Class_Doesnt_Match_Control_With_Class()
{
var control = new Control1
{
Classes = new Classes { "foo" },
};
var target = default(Selector).Not(x => x.Class("foo"));
var match = target.Match(control);
Assert.Equal(SelectorMatchResult.Sometimes, match.Result);
Assert.False(await match.Activator.Take(1));
}
[Fact]
public async Task Not_Class_Matches_Control_Without_Class()
{
var control = new Control1
{
Classes = new Classes { "bar" },
};
var target = default(Selector).Not(x => x.Class("foo"));
var match = target.Match(control);
Assert.Equal(SelectorMatchResult.Sometimes, match.Result);
Assert.True(await match.Activator.Take(1));
}
[Fact]
public async Task OfType_Not_Class_Matches_Control_Without_Class()
{
var control = new Control1
{
Classes = new Classes { "bar" },
};
var target = default(Selector).OfType<Control1>().Not(x => x.Class("foo"));
var match = target.Match(control);
Assert.Equal(SelectorMatchResult.Sometimes, match.Result);
Assert.True(await match.Activator.Take(1));
}
[Fact]
public void OfType_Not_Class_Doesnt_Match_Control_Of_Wrong_Type()
{
var control = new Control2
{
Classes = new Classes { "foo" },
};
var target = default(Selector).OfType<Control1>().Not(x => x.Class("foo"));
var match = target.Match(control);
Assert.Equal(SelectorMatchResult.NeverThisType, match.Result);
}
[Fact]
public void Returns_Correct_TargetType()
{
var target = default(Selector).OfType<Control1>().Not(x => x.Class("foo"));
Assert.Equal(typeof(Control1), target.TargetType);
}
public class Control1 : TestControlBase
{
}
public class Control2 : TestControlBase
{
}
}
}

1
tests/Avalonia.Styling.UnitTests/StyledElementTests.cs

@ -10,6 +10,7 @@ using Avalonia.UnitTests;
using Xunit;
using Avalonia.LogicalTree;
using Avalonia.Controls;
using System.ComponentModel;
namespace Avalonia.Styling.UnitTests
{

Loading…
Cancel
Save