Browse Source

Merge branch 'master' into jmacato/missing-skia-api-lottie

pull/5683/head
Jumar Macato 6 years ago
committed by GitHub
parent
commit
a6b35ec20c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      native/Avalonia.Native/src/OSX/common.h
  2. 4
      native/Avalonia.Native/src/OSX/main.mm
  3. 9
      native/Avalonia.Native/src/OSX/menu.h
  4. 34
      native/Avalonia.Native/src/OSX/menu.mm
  5. 4
      nukebuild/Build.cs
  6. 4
      samples/ControlCatalog/MainWindow.xaml
  7. 28
      src/Avalonia.Animation/Animatable.cs
  8. 9
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  9. 9
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  10. 2
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  11. 49
      src/Avalonia.Base/ValueStore.cs
  12. 4
      src/Avalonia.Controls/ApiCompatBaseline.txt
  13. 1
      src/Avalonia.Controls/Button.cs
  14. 2
      src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs
  15. 33
      src/Avalonia.Controls/NativeMenu.cs
  16. 16
      src/Avalonia.Controls/NativeMenuItemSeparator.cs
  17. 10
      src/Avalonia.Controls/NativeMenuItemSeperator.cs
  18. 22
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  19. 2
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  20. 26
      src/Avalonia.Native/IAvnMenu.cs
  21. 7
      src/Avalonia.Native/avn.idl
  22. 2
      src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
  23. 31
      tests/Avalonia.Animation.UnitTests/AnimatableTests.cs
  24. 122
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
  25. 64
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  26. 29
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  27. 26
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

2
native/Avalonia.Native/src/OSX/common.h

@ -23,7 +23,7 @@ extern IAvnCursorFactory* CreateCursorFactory();
extern IAvnGlDisplay* GetGlDisplay();
extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events);
extern IAvnMenuItem* CreateAppMenuItem();
extern IAvnMenuItem* CreateAppMenuItemSeperator();
extern IAvnMenuItem* CreateAppMenuItemSeparator();
extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);
extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu);
extern IAvnMenu* GetAppMenu ();

4
native/Avalonia.Native/src/OSX/main.mm

@ -253,9 +253,9 @@ public:
return S_OK;
}
virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) override
virtual HRESULT CreateMenuItemSeparator (IAvnMenuItem** ppv) override
{
*ppv = ::CreateAppMenuItemSeperator();
*ppv = ::CreateAppMenuItemSeparator();
return S_OK;
}

9
native/Avalonia.Native/src/OSX/menu.h

@ -31,13 +31,13 @@ private:
NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem
IAvnActionCallback* _callback;
IAvnPredicateCallback* _predicate;
bool _isSeperator;
bool _isSeparator;
bool _isCheckable;
public:
FORWARD_IUNKNOWN()
AvnAppMenuItem(bool isSeperator);
AvnAppMenuItem(bool isSeparator);
NSMenuItem* GetNative();
@ -60,7 +60,6 @@ public:
void RaiseOnClicked();
};
class AvnAppMenu : public ComSingleObject<IAvnMenu, &IID_IAvnMenu>
{
private:
@ -71,10 +70,12 @@ public:
FORWARD_IUNKNOWN()
AvnAppMenu(IAvnMenuEvents* events);
AvnMenu* GetNative();
void RaiseNeedsUpdate ();
void RaiseOpening();
void RaiseClosed();
virtual HRESULT InsertItem (int index, IAvnMenuItem* item) override;

34
native/Avalonia.Native/src/OSX/menu.mm

@ -71,12 +71,12 @@
}
@end
AvnAppMenuItem::AvnAppMenuItem(bool isSeperator)
AvnAppMenuItem::AvnAppMenuItem(bool isSeparator)
{
_isCheckable = false;
_isSeperator = isSeperator;
_isSeparator = isSeparator;
if(isSeperator)
if(isSeparator)
{
_native = [NSMenuItem separatorItem];
}
@ -298,6 +298,23 @@ void AvnAppMenu::RaiseNeedsUpdate()
}
}
void AvnAppMenu::RaiseOpening()
{
if(_baseEvents != nullptr)
{
_baseEvents->Opening();
}
}
void AvnAppMenu::RaiseClosed()
{
if(_baseEvents != nullptr)
{
_baseEvents->Closed();
}
}
HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item)
{
@autoreleasepool
@ -382,6 +399,15 @@ HRESULT AvnAppMenu::Clear()
_parent->RaiseNeedsUpdate();
}
- (void)menuWillOpen:(NSMenu *)menu
{
_parent->RaiseOpening();
}
- (void)menuDidClose:(NSMenu *)menu
{
_parent->RaiseClosed();
}
@end
@ -401,7 +427,7 @@ extern IAvnMenuItem* CreateAppMenuItem()
}
}
extern IAvnMenuItem* CreateAppMenuItemSeperator()
extern IAvnMenuItem* CreateAppMenuItemSeparator()
{
@autoreleasepool
{

4
nukebuild/Build.cs

@ -89,10 +89,6 @@ partial class Build : NukeBuild
Process.Start(new ProcessStartInfo(command, args) {UseShellExecute = false}).WaitForExit();
}
ExecWait("dotnet version:", "dotnet", "--version");
if (Parameters.IsRunningOnUnix)
ExecWait("Mono version:", "mono", "--version");
}
IReadOnlyCollection<Output> MsBuildCommon(

4
samples/ControlCatalog/MainWindow.xaml

@ -18,11 +18,11 @@
<NativeMenuItem Header="File">
<NativeMenu>
<NativeMenuItem Icon="/Assets/test_icon.ico" Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
<NativeMenuItemSeperator/>
<NativeMenuItemSeperator/><!-- Uses incorrect spelling to demonstrate backwards compatibility -->
<NativeMenuItem Icon="/Assets/github_icon.png" Header="Recent">
<NativeMenu/>
</NativeMenuItem>
<NativeMenuItemSeperator/>
<NativeMenuItemSeparator/>
<NativeMenuItem Header="{x:Static local:MainWindow.MenuQuitHeader}"
Gesture="{x:Static local:MainWindow.MenuQuitGesture}"
Clicked="OnCloseClicked" />

28
src/Avalonia.Animation/Animatable.cs

@ -2,6 +2,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Data;
#nullable enable
@ -93,16 +94,35 @@ namespace Avalonia.Animation
var oldTransitions = change.OldValue.GetValueOrDefault<Transitions>();
var newTransitions = change.NewValue.GetValueOrDefault<Transitions>();
// When transitions are replaced, we add the new transitions before removing the old
// transitions, so that when the old transition being disposed causes the value to
// change, there is a corresponding entry in `_transitionStates`. This means that we
// need to account for any transitions present in both the old and new transitions
// collections.
if (newTransitions is object)
{
var toAdd = (IList)newTransitions;
if (newTransitions.Count > 0 && oldTransitions?.Count > 0)
{
toAdd = newTransitions.Except(oldTransitions).ToList();
}
newTransitions.CollectionChanged += TransitionsCollectionChanged;
AddTransitions(newTransitions);
AddTransitions(toAdd);
}
if (oldTransitions is object)
{
var toRemove = (IList)oldTransitions;
if (oldTransitions.Count > 0 && newTransitions?.Count > 0)
{
toRemove = oldTransitions.Except(newTransitions).ToList();
}
oldTransitions.CollectionChanged -= TransitionsCollectionChanged;
RemoveTransitions(oldTransitions);
RemoveTransitions(toRemove);
}
}
else if (_transitionsEnabled &&
@ -115,9 +135,9 @@ namespace Avalonia.Animation
{
var transition = Transitions[i];
if (transition.Property == change.Property)
if (transition.Property == change.Property &&
_transitionState.TryGetValue(transition, out var state))
{
var state = _transitionState[transition];
var oldValue = state.BaseValue;
var newValue = GetAnimationBaseValue(transition.Property);

9
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@ -65,7 +65,6 @@ namespace Avalonia.PropertyStore
{
_subscription?.Dispose();
_subscription = null;
_isSubscribed = false;
OnCompleted();
}
@ -74,6 +73,7 @@ namespace Avalonia.PropertyStore
var oldValue = _value;
_value = default;
Priority = BindingPriority.Unset;
_isSubscribed = false;
_sink.Completed(Property, this, oldValue);
}
@ -104,8 +104,11 @@ namespace Avalonia.PropertyStore
public void Start(bool ignoreBatchUpdate)
{
// We can't use _subscription to check whether we're subscribed because it won't be set
// until Subscribe has finished, which will be too late to prevent reentrancy.
if (!_isSubscribed && (!_batchUpdate || ignoreBatchUpdate))
// until Subscribe has finished, which will be too late to prevent reentrancy. In addition
// don't re-subscribe to completed/disposed bindings (indicated by Unset priority).
if (!_isSubscribed &&
Priority != BindingPriority.Unset &&
(!_batchUpdate || ignoreBatchUpdate))
{
_isSubscribed = true;
_subscription = Source.Subscribe(this);

9
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@ -6,12 +6,19 @@ using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an untyped interface to <see cref="ConstantValueEntry{T}"/>.
/// </summary>
internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable
{
}
/// <summary>
/// Stores a value with a priority in a <see cref="ValueStore"/> or
/// <see cref="PriorityValue{T}"/>.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IDisposable
internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IConstantValueEntry
{
private IValueSink _sink;
private Optional<T> _value;

2
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@ -94,7 +94,7 @@ namespace Avalonia.Utilities
return (0, false);
}
public bool TryGetValue(AvaloniaProperty property, [MaybeNull] out TValue value)
public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)

49
src/Avalonia.Base/ValueStore.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Utilities;
@ -56,7 +57,7 @@ namespace Avalonia
public bool IsAnimating(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var slot))
if (TryGetValue(property, out var slot))
{
return slot.Priority < BindingPriority.LocalValue;
}
@ -66,7 +67,7 @@ namespace Avalonia
public bool IsSet(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var slot))
if (TryGetValue(property, out var slot))
{
return slot.GetValue().HasValue;
}
@ -79,7 +80,7 @@ namespace Avalonia
BindingPriority maxPriority,
out T value)
{
if (_values.TryGetValue(property, out var slot))
if (TryGetValue(property, out var slot))
{
var v = ((IValue<T>)slot).GetValue(maxPriority);
@ -103,7 +104,7 @@ namespace Avalonia
IDisposable? result = null;
if (_values.TryGetValue(property, out var slot))
if (TryGetValue(property, out var slot))
{
result = SetExisting(slot, property, value, priority);
}
@ -138,7 +139,7 @@ namespace Avalonia
IObservable<BindingValue<T>> source,
BindingPriority priority)
{
if (_values.TryGetValue(property, out var slot))
if (TryGetValue(property, out var slot))
{
return BindExisting(slot, property, source, priority);
}
@ -160,7 +161,7 @@ namespace Avalonia
public void ClearLocalValue<T>(StyledPropertyBase<T> property)
{
if (_values.TryGetValue(property, out var slot))
if (TryGetValue(property, out var slot))
{
if (slot is PriorityValue<T> p)
{
@ -173,7 +174,7 @@ namespace Avalonia
// During batch update values can't be removed immediately because they're needed to raise
// a correctly-typed _sink.ValueChanged notification. They instead mark themselves for removal
// by setting their priority to Unset.
if (_batchUpdate is null)
if (!IsBatchUpdating())
{
_values.Remove(property);
}
@ -198,7 +199,7 @@ namespace Avalonia
public void CoerceValue<T>(StyledPropertyBase<T> property)
{
if (_values.TryGetValue(property, out var slot))
if (TryGetValue(property, out var slot))
{
if (slot is PriorityValue<T> p)
{
@ -209,7 +210,7 @@ namespace Avalonia
public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var slot))
if (TryGetValue(property, out var slot))
{
var slotValue = slot.GetValue();
return new Diagnostics.AvaloniaPropertyValue(
@ -242,6 +243,7 @@ namespace Avalonia
IPriorityValueEntry entry,
Optional<T> oldValue)
{
// We need to include remove sentinels here so call `_values.TryGetValue` directly.
if (_values.TryGetValue(property, out var slot) && slot == entry)
{
if (_batchUpdate is null)
@ -285,7 +287,7 @@ namespace Avalonia
else
{
var priorityValue = new PriorityValue<T>(_owner, property, this, l);
if (_batchUpdate is object)
if (IsBatchUpdating())
priorityValue.BeginBatchUpdate();
result = priorityValue.SetValue(value, priority);
_values.SetValue(property, priorityValue);
@ -311,7 +313,7 @@ namespace Avalonia
{
priorityValue = new PriorityValue<T>(_owner, property, this, e);
if (_batchUpdate is object)
if (IsBatchUpdating())
{
priorityValue.BeginBatchUpdate();
}
@ -338,7 +340,7 @@ namespace Avalonia
private void AddValue(AvaloniaProperty property, IValue value)
{
_values.AddValue(property, value);
if (_batchUpdate is object && value is IBatchUpdate batch)
if (IsBatchUpdating() && value is IBatchUpdate batch)
batch.BeginBatchUpdate();
value.Start();
}
@ -364,6 +366,21 @@ namespace Avalonia
}
}
private bool IsBatchUpdating() => _batchUpdate?.IsBatchUpdating == true;
private bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out IValue value)
{
return _values.TryGetValue(property, out value) && !IsRemoveSentinel(value);
}
private static bool IsRemoveSentinel(IValue value)
{
// Local value entries are optimized and contain only a single value field to save space,
// so there's no way to mark them for removal at the end of a batch update. Instead a
// ConstantValueEntry with a priority of Unset is used as a sentinel value.
return value is IConstantValueEntry t && t.Priority == BindingPriority.Unset;
}
private class BatchUpdate
{
private ValueStore _owner;
@ -373,6 +390,8 @@ namespace Avalonia
public BatchUpdate(ValueStore owner) => _owner = owner;
public bool IsBatchUpdating => _batchUpdateCount > 0;
public void Begin()
{
if (_batchUpdateCount++ == 0)
@ -437,8 +456,10 @@ namespace Avalonia
// During batch update values can't be removed immediately because they're needed to raise
// the _sink.ValueChanged notification. They instead mark themselves for removal by setting
// their priority to Unset.
if (slot.Priority == BindingPriority.Unset)
// their priority to Unset. We need to re-read the slot here because raising ValueChanged
// could have caused it to be updated.
if (values.TryGetValue(entry.property, out var updatedSlot) &&
updatedSlot.Priority == BindingPriority.Unset)
{
values.Remove(entry.property);
}

4
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -1,7 +1,9 @@
Compat issues with assembly Avalonia.Controls:
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
Total Issues: 5
Total Issues: 7

1
src/Avalonia.Controls/Button.cs

@ -218,6 +218,7 @@ namespace Avalonia.Controls
if (Command != null)
{
Command.CanExecuteChanged += CanExecuteChanged;
CanExecuteChanged(this, EventArgs.Empty);
}
}

2
src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs

@ -3,5 +3,7 @@ namespace Avalonia.Controls
public interface INativeMenuExporterEventsImplBridge
{
void RaiseNeedsUpdate ();
void RaiseOpening();
void RaiseClosed();
}
}

33
src/Avalonia.Controls/NativeMenu.cs

@ -12,13 +12,34 @@ namespace Avalonia.Controls
private readonly AvaloniaList<NativeMenuItemBase> _items =
new AvaloniaList<NativeMenuItemBase> { ResetBehavior = ResetBehavior.Remove };
private NativeMenuItem _parent;
[Content]
public IList<NativeMenuItemBase> Items => _items;
/// <summary>
/// Raised when the user clicks the menu and before its opened. Use this event to update the menu dynamically.
/// Raised when the menu requests an update.
/// </summary>
/// <remarks>
/// Use this event to add, remove or modify menu items before a menu is
/// shown or a hotkey is pressed.
/// </remarks>
public event EventHandler<EventArgs> NeedsUpdate;
/// <summary>
/// Raised before the menu is opened.
/// </summary>
/// <remarks>
/// Do not update the menu in this event; use <see cref="NeedsUpdate"/>.
/// </remarks>
public event EventHandler<EventArgs> Opening;
/// <summary>
/// Raised after the menu is closed.
/// </summary>
/// <remarks>
/// Do not update the menu in this event; use <see cref="NeedsUpdate"/>.
/// </remarks>
public event EventHandler<EventArgs> Closed;
public NativeMenu()
{
@ -27,10 +48,20 @@ namespace Avalonia.Controls
}
void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate()
{
NeedsUpdate?.Invoke(this, EventArgs.Empty);
}
void INativeMenuExporterEventsImplBridge.RaiseOpening()
{
Opening?.Invoke(this, EventArgs.Empty);
}
void INativeMenuExporterEventsImplBridge.RaiseClosed()
{
Closed?.Invoke(this, EventArgs.Empty);
}
private void Validator(NativeMenuItemBase obj)
{
if (obj.Parent != null)

16
src/Avalonia.Controls/NativeMenuItemSeparator.cs

@ -0,0 +1,16 @@
using System;
namespace Avalonia.Controls
{
[Obsolete("This class exists to maintain backwards compatiblity with existing code. Use NativeMenuItemSeparator instead")]
public class NativeMenuItemSeperator : NativeMenuItemSeparator
{
}
public class NativeMenuItemSeparator : NativeMenuItemBase
{
[Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)]
public string Header => "-";
}
}

10
src/Avalonia.Controls/NativeMenuItemSeperator.cs

@ -1,10 +0,0 @@
using System;
namespace Avalonia.Controls
{
public class NativeMenuItemSeperator : NativeMenuItemBase
{
[Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)]
public string Header => "-";
}
}

22
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives
nameof(SelectedItem),
o => o.SelectedItem,
(o, v) => o.SelectedItem = v,
defaultBindingMode: BindingMode.TwoWay);
defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
/// <summary>
/// Defines the <see cref="SelectedItems"/> property.
@ -466,6 +466,20 @@ namespace Avalonia.Controls.Primitives
EndUpdating();
}
/// <summary>
/// Called to update the validation state for properties for which data validation is
/// enabled.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The new binding value for the property.</param>
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
if (property == SelectedItemProperty)
{
DataValidationErrors.SetError(this, value.Error);
}
}
protected override void OnInitialized()
{
base.OnInitialized();
@ -707,7 +721,7 @@ namespace Avalonia.Controls.Primitives
_oldSelectedItem = SelectedItem;
}
else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) &&
_oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems)
_oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems)
{
RaisePropertyChanged(
SelectedItemsProperty,
@ -977,7 +991,7 @@ namespace Avalonia.Controls.Primitives
public Optional<ISelectionModel> Selection { get; set; }
public Optional<IList?> SelectedItems { get; set; }
public Optional<int> SelectedIndex
public Optional<int> SelectedIndex
{
get => _selectedIndex;
set
@ -996,6 +1010,6 @@ namespace Avalonia.Controls.Primitives
_selectedIndex = default;
}
}
}
}
}
}

2
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@ -192,7 +192,7 @@ namespace Avalonia.FreeDesktop
{
var (it, menu) = i;
if (it is NativeMenuItemSeperator)
if (it is NativeMenuItemSeparator)
{
if (name == "type")
return "separator";

26
src/Avalonia.Native/IAvnMenu.cs

@ -20,11 +20,23 @@ namespace Avalonia.Native.Interop
{
_parent?.RaiseNeedsUpdate();
}
public void Opening()
{
_parent?.RaiseOpening();
}
public void Closed()
{
_parent?.RaiseClosed();
}
}
partial interface IAvnMenu
{
void RaiseNeedsUpdate();
void RaiseOpening();
void RaiseClosed();
void Deinitialise();
}
}
@ -45,6 +57,16 @@ namespace Avalonia.Native.Interop.Impl
_exporter.UpdateIfNeeded();
}
public void RaiseOpening()
{
(ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseOpening();
}
public void RaiseClosed()
{
(ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseClosed();
}
internal NativeMenu ManagedMenu { get; private set; }
public static __MicroComIAvnMenuProxy Create(IAvaloniaNativeFactory factory)
@ -103,8 +125,8 @@ namespace Avalonia.Native.Interop.Impl
private __MicroComIAvnMenuItemProxy CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item)
{
var nativeItem = (__MicroComIAvnMenuItemProxy)(item is NativeMenuItemSeperator ?
factory.CreateMenuItemSeperator() :
var nativeItem = (__MicroComIAvnMenuItemProxy)(item is NativeMenuItemSeparator ?
factory.CreateMenuItemSeparator() :
factory.CreateMenuItem());
nativeItem.ManagedMenuItem = item;

7
src/Avalonia.Native/avn.idl

@ -417,7 +417,7 @@ interface IAvaloniaNativeFactory : IUnknown
HRESULT SetAppMenu(IAvnMenu* menu);
HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv);
HRESULT CreateMenuItem(IAvnMenuItem** ppv);
HRESULT CreateMenuItemSeperator(IAvnMenuItem** ppv);
HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv);
}
[uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)]
@ -685,10 +685,9 @@ interface IAvnMenuItem : IUnknown
[uuid(0af7df53-7632-42f4-a650-0992c361b477)]
interface IAvnMenuEvents : IUnknown
{
/**
* NeedsUpdate
*/
void NeedsUpdate();
void Opening();
void Closed();
}
[uuid(5142bb41-66ab-49e7-bb37-cd079c000f27)]

2
src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml

@ -35,7 +35,7 @@
<ItemsControl Items="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Message}" Foreground="{DynamicResource SystemControlErrorTextForegroundBrush}" TextWrapping="Wrap" />
<TextBlock Text="{Binding }" Foreground="{DynamicResource SystemControlErrorTextForegroundBrush}" TextWrapping="Wrap" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

31
tests/Avalonia.Animation.UnitTests/AnimatableTests.cs

@ -330,6 +330,37 @@ namespace Avalonia.Animation.UnitTests
}
}
[Fact]
public void Transitions_Can_Be_Changed_To_Collection_That_Contains_The_Same_Transitions()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
control.Transitions = new Transitions { target.Object };
}
[Fact]
public void Transitions_Can_Re_Set_During_Batch_Update()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
// Assigning and then clearing Transitions ensures we have a transition state
// collection created.
control.Transitions = null;
control.BeginBatchUpdate();
// Setting opacity then Transitions means that we receive the Transitions change
// after the Opacity change when EndBatchUpdate is called.
control.Opacity = 0.5;
control.Transitions = new Transitions { target.Object };
// Which means that the transition state hasn't been initialized with the new
// Transitions when the Opacity change notification gets raised here.
control.EndBatchUpdate();
}
private static Mock<ITransition> CreateTarget()
{
return CreateTransition(Visual.OpacityProperty);

122
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs

@ -53,6 +53,21 @@ namespace Avalonia.Base.UnitTests
Assert.Empty(raised);
}
[Fact]
public void Binding_Disposal_Should_Not_Raise_Property_Changes_During_Batch_Update()
{
var target = new TestClass();
var observable = new TestObservable<string>("foo");
var raised = new List<string>();
var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
target.BeginBatchUpdate();
sub.Dispose();
Assert.Empty(raised);
}
[Fact]
public void SetValue_Change_Should_Be_Raised_After_Batch_Update_1()
{
@ -240,6 +255,27 @@ namespace Avalonia.Base.UnitTests
Assert.Equal(BindingPriority.Unset, raised[0].Priority);
}
[Fact]
public void Binding_Disposal_Should_Be_Raised_After_Batch_Update()
{
var target = new TestClass();
var observable = new TestObservable<string>("foo");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
sub.Dispose();
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Null(target.Foo);
Assert.Equal("foo", raised[0].OldValue);
Assert.Null(raised[0].NewValue);
Assert.Equal(BindingPriority.Unset, raised[0].Priority);
}
[Fact]
public void ClearValue_Change_Should_Be_Raised_After_Batch_Update_1()
{
@ -449,6 +485,92 @@ namespace Avalonia.Base.UnitTests
Assert.Null(raised[1].NewValue);
}
[Fact]
public void Can_Set_Cleared_Value_When_Ending_Batch_Update()
{
var target = new TestClass();
var raised = 0;
target.Foo = "foo";
target.BeginBatchUpdate();
target.ClearValue(TestClass.FooProperty);
target.PropertyChanged += (sender, e) =>
{
if (e.Property == TestClass.FooProperty && e.NewValue is null)
{
target.Foo = "bar";
++raised;
}
};
target.EndBatchUpdate();
Assert.Equal("bar", target.Foo);
Assert.Equal(1, raised);
}
[Fact]
public void Can_Bind_Cleared_Value_When_Ending_Batch_Update()
{
var target = new TestClass();
var raised = 0;
var notifications = new List<AvaloniaPropertyChangedEventArgs>();
target.Foo = "foo";
target.BeginBatchUpdate();
target.ClearValue(TestClass.FooProperty);
target.PropertyChanged += (sender, e) =>
{
if (e.Property == TestClass.FooProperty && e.NewValue is null)
{
target.Bind(TestClass.FooProperty, new TestObservable<string>("bar"));
++raised;
}
notifications.Add(e);
};
target.EndBatchUpdate();
Assert.Equal("bar", target.Foo);
Assert.Equal(1, raised);
Assert.Equal(2, notifications.Count);
Assert.Equal(null, notifications[0].NewValue);
Assert.Equal("bar", notifications[1].NewValue);
}
[Fact]
public void Can_Bind_Completed_Binding_Back_To_Original_Value_When_Ending_Batch_Update()
{
var target = new TestClass();
var raised = 0;
var notifications = new List<AvaloniaPropertyChangedEventArgs>();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("foo");
target.Bind(TestClass.FooProperty, observable1);
target.BeginBatchUpdate();
observable1.OnCompleted();
target.PropertyChanged += (sender, e) =>
{
if (e.Property == TestClass.FooProperty && e.NewValue is null)
{
target.Bind(TestClass.FooProperty, observable2);
++raised;
}
notifications.Add(e);
};
target.EndBatchUpdate();
Assert.Equal("foo", target.Foo);
Assert.Equal(1, raised);
Assert.Equal(2, notifications.Count);
Assert.Equal(null, notifications[0].NewValue);
Assert.Equal("foo", notifications[1].NewValue);
}
public class TestClass : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =

64
tests/Avalonia.Controls.UnitTests/CarouselTests.cs

@ -1,10 +1,14 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.LogicalTree;
using Avalonia.Threading;
using Avalonia.VisualTree;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests
@ -77,9 +81,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
@ -113,9 +117,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
@ -172,9 +176,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
@ -206,9 +210,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
@ -235,9 +239,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
@ -269,9 +273,9 @@ namespace Avalonia.Controls.UnitTests
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
@ -311,5 +315,29 @@ namespace Avalonia.Controls.UnitTests
contentPresenter.UpdateChild();
return Assert.IsType<TextBlock>(contentPresenter.Child);
}
[Fact]
public void SelectedItem_Validation()
{
using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
{
var target = new Carousel
{
Template = new FuncControlTemplate<Carousel>(CreateTemplate), IsVirtualized = false
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var exception = new System.InvalidCastException("failed validation");
var textObservable =
new BehaviorSubject<BindingNotification>(new BindingNotification(exception,
BindingErrorType.DataValidationError));
target.Bind(ComboBox.SelectedItemProperty, textObservable);
Assert.True(DataValidationErrors.GetHasErrors(target));
Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
}
}
}
}

29
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@ -1,11 +1,14 @@
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Xunit;
@ -173,5 +176,31 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(expectedSelectedIndex, target.SelectedIndex);
}
}
[Fact]
public void SelectedItem_Validation()
{
using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
{
var target = new ComboBox
{
Template = GetTemplate(),
VirtualizationMode = ItemVirtualizationMode.None
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var exception = new System.InvalidCastException("failed validation");
var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
target.Bind(ComboBox.SelectedItemProperty, textObservable);
Assert.True(DataValidationErrors.GetHasErrors(target));
Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
}
}
}
}

26
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -1,11 +1,14 @@
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Styling;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
@ -559,5 +562,28 @@ namespace Avalonia.Controls.UnitTests
public string Value { get; }
}
[Fact]
public void SelectedItem_Validation()
{
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = new[] { "Foo" },
ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas()),
SelectionMode = SelectionMode.AlwaysSelected,
VirtualizationMode = ItemVirtualizationMode.None
};
Prepare(target);
var exception = new System.InvalidCastException("failed validation");
var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
target.Bind(ComboBox.SelectedItemProperty, textObservable);
Assert.True(DataValidationErrors.GetHasErrors(target));
Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
}
}
}

Loading…
Cancel
Save