diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 0f7215f37c..c082003ccf 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/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 (); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 11742e3b5c..aaaf381b26 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/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; } diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index 564fdc132b..186fcf255b 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/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 { 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; diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index ea5cca9ce8..b9a95e7b3c 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/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 { diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 8e331edab4..d627a2bf19 100644 --- a/nukebuild/Build.cs +++ b/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 MsBuildCommon( diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 6a70bb082f..a107ee2163 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -18,11 +18,11 @@ - + - + diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 067d9f462f..a415046513 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/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(); var newTransitions = change.NewValue.GetValueOrDefault(); + // 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); diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index 38c1728cd9..3e17a81dd8 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/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); diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs index 600d725187..d39fc3bb1e 100644 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -6,12 +6,19 @@ using Avalonia.Data; namespace Avalonia.PropertyStore { + /// + /// Represents an untyped interface to . + /// + internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable + { + } + /// /// Stores a value with a priority in a or /// . /// /// The property type. - internal class ConstantValueEntry : IPriorityValueEntry, IDisposable + internal class ConstantValueEntry : IPriorityValueEntry, IConstantValueEntry { private IValueSink _sink; private Optional _value; diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs index 1af6f21156..c513f75962 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs +++ b/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) diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index e32b20cc96..495f13e1a9 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/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)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> 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(StyledPropertyBase property) { - if (_values.TryGetValue(property, out var slot)) + if (TryGetValue(property, out var slot)) { if (slot is PriorityValue 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(StyledPropertyBase property) { - if (_values.TryGetValue(property, out var slot)) + if (TryGetValue(property, out var slot)) { if (slot is PriorityValue 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 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(_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(_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); } diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index aa8db78087..0284463f1c 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/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 diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index c779e4b0cb..6093cbd581 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -218,6 +218,7 @@ namespace Avalonia.Controls if (Command != null) { Command.CanExecuteChanged += CanExecuteChanged; + CanExecuteChanged(this, EventArgs.Empty); } } diff --git a/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs b/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs index 672d5c1a13..f492e6ca0f 100644 --- a/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs +++ b/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs @@ -3,5 +3,7 @@ namespace Avalonia.Controls public interface INativeMenuExporterEventsImplBridge { void RaiseNeedsUpdate (); + void RaiseOpening(); + void RaiseClosed(); } } diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 38a9f03d29..58ee99722f 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -12,13 +12,34 @@ namespace Avalonia.Controls private readonly AvaloniaList _items = new AvaloniaList { ResetBehavior = ResetBehavior.Remove }; private NativeMenuItem _parent; + [Content] public IList Items => _items; /// - /// 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. + /// + /// + /// Use this event to add, remove or modify menu items before a menu is + /// shown or a hotkey is pressed. + /// + public event EventHandler NeedsUpdate; + + /// + /// Raised before the menu is opened. /// + /// + /// Do not update the menu in this event; use . + /// public event EventHandler Opening; + + /// + /// Raised after the menu is closed. + /// + /// + /// Do not update the menu in this event; use . + /// + public event EventHandler 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) diff --git a/src/Avalonia.Controls/NativeMenuItemSeparator.cs b/src/Avalonia.Controls/NativeMenuItemSeparator.cs new file mode 100644 index 0000000000..d3d3721c89 --- /dev/null +++ b/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 => "-"; + } +} diff --git a/src/Avalonia.Controls/NativeMenuItemSeperator.cs b/src/Avalonia.Controls/NativeMenuItemSeperator.cs deleted file mode 100644 index e743483dab..0000000000 --- a/src/Avalonia.Controls/NativeMenuItemSeperator.cs +++ /dev/null @@ -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 => "-"; - } -} diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 2cd69793dc..19824a71f0 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/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); /// /// Defines the property. @@ -466,6 +466,20 @@ namespace Avalonia.Controls.Primitives EndUpdating(); } + /// + /// Called to update the validation state for properties for which data validation is + /// enabled. + /// + /// The property. + /// The new binding value for the property. + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue 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 Selection { get; set; } public Optional SelectedItems { get; set; } - public Optional SelectedIndex + public Optional SelectedIndex { get => _selectedIndex; set @@ -996,6 +1010,6 @@ namespace Avalonia.Controls.Primitives _selectedIndex = default; } } - } + } } } diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 2cf533195e..9e635e01f1 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/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"; diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index dd9464284f..f76e9450fc 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/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; diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 2693f5f139..9cada1120a 100644 --- a/src/Avalonia.Native/avn.idl +++ b/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)] diff --git a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml index ed31f7b573..f83af266c2 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml @@ -35,7 +35,7 @@ - + diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs index 7633a761a3..b01fb70f58 100644 --- a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs +++ b/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 CreateTarget() { return CreateTransition(Visual.OpacityProperty); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs index 9f0e52c8d9..53ad87421e 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs +++ b/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("foo"); + var raised = new List(); + + 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("foo"); + var raised = new List(); + + 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(); + + 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("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(); + var observable1 = new TestObservable("foo"); + var observable2 = new TestObservable("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 FooProperty = diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index 051f6c3fd3..b93e48618d 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/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 { - "Foo", - "Bar", - "FooBar" + "Foo", + "Bar", + "FooBar" }; var target = new Carousel @@ -113,9 +117,9 @@ namespace Avalonia.Controls.UnitTests { var items = new ObservableCollection { - "Foo", - "Bar", - "FooBar" + "Foo", + "Bar", + "FooBar" }; var target = new Carousel @@ -172,9 +176,9 @@ namespace Avalonia.Controls.UnitTests { var items = new ObservableCollection { - "Foo", - "Bar", - "FooBar" + "Foo", + "Bar", + "FooBar" }; var target = new Carousel @@ -206,9 +210,9 @@ namespace Avalonia.Controls.UnitTests { var items = new ObservableCollection { - "Foo", - "Bar", - "FooBar" + "Foo", + "Bar", + "FooBar" }; var target = new Carousel @@ -235,9 +239,9 @@ namespace Avalonia.Controls.UnitTests { var items = new ObservableCollection { - "Foo", - "Bar", - "FooBar" + "Foo", + "Bar", + "FooBar" }; var target = new Carousel @@ -269,9 +273,9 @@ namespace Avalonia.Controls.UnitTests { var items = new ObservableCollection { - "Foo", - "Bar", - "FooBar" + "Foo", + "Bar", + "FooBar" }; var target = new Carousel @@ -311,5 +315,29 @@ namespace Avalonia.Controls.UnitTests contentPresenter.UpdateChild(); return Assert.IsType(contentPresenter.Child); } + + [Fact] + public void SelectedItem_Validation() + { + using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) + { + var target = new Carousel + { + Template = new FuncControlTemplate(CreateTemplate), IsVirtualized = false + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + var exception = new System.InvalidCastException("failed validation"); + var textObservable = + new BehaviorSubject(new BindingNotification(exception, + BindingErrorType.DataValidationError)); + target.Bind(ComboBox.SelectedItemProperty, textObservable); + + Assert.True(DataValidationErrors.GetHasErrors(target)); + Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 4ea838358c..8f9c7fdb0b 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/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(new BindingNotification(exception, BindingErrorType.DataValidationError)); + target.Bind(ComboBox.SelectedItemProperty, textObservable); + + Assert.True(DataValidationErrors.GetHasErrors(target)); + Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); + + } + + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 145fce4fed..963bba7c83 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/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((_, __) => new Canvas()), + SelectionMode = SelectionMode.AlwaysSelected, + VirtualizationMode = ItemVirtualizationMode.None + }; + + Prepare(target); + + var exception = new System.InvalidCastException("failed validation"); + var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError)); + target.Bind(ComboBox.SelectedItemProperty, textObservable); + + Assert.True(DataValidationErrors.GetHasErrors(target)); + Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); + } } }