diff --git a/build.cake b/build.cake index ba53e4387e..f1d2e87910 100644 --- a/build.cake +++ b/build.cake @@ -5,10 +5,9 @@ #tool dotnet:?package=NuGetKeyVaultSignTool&version=3.2.3 #tool dotnet:?package=AzureSignTool&version=4.0.1 #tool dotnet:?package=GitReleaseManager.Tool&version=0.15.0 -#tool dotnet:?package=XamlStyler.Console&version=3.2206.4 - +#tool dotnet:?package=XamlStyler.Console&version=3.2404.2 #tool nuget:?package=GitVersion.CommandLine&version=5.12.0 -#tool nuget:?package=xunit.runner.console&version=2.9.2 +#tool nuget:?package=NUnit.ConsoleRunner&version=3.18.3 #addin nuget:?package=Cake.Figlet&version=2.0.1 diff --git a/src/Directory.packages.props b/src/Directory.packages.props index 363615d7ff..482875aeb2 100644 --- a/src/Directory.packages.props +++ b/src/Directory.packages.props @@ -28,10 +28,9 @@ - - - - - + + + + \ No newline at end of file diff --git a/src/MahApps.Metro.sln b/src/MahApps.Metro.sln index c7fc2eea31..b99063f639 100644 --- a/src/MahApps.Metro.sln +++ b/src/MahApps.Metro.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28711.60 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35312.102 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MahApps.Metro", "MahApps.Metro\MahApps.Metro.csproj", "{F6432F90-04C2-42B9-992B-CD828729976B}" EndProject @@ -14,6 +14,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EC5373FC-2098-4D7B-8990-B15E9C631AE8}" ProjectSection(SolutionItems) = preProject ..\.editorconfig = ..\.editorconfig + ..\build.cake = ..\build.cake Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets Directory.packages.props = Directory.packages.props diff --git a/src/MahApps.Metro/Controls/MultiSelectionComboBox/MultiSelectionComboBox.cs b/src/MahApps.Metro/Controls/MultiSelectionComboBox/MultiSelectionComboBox.cs index 0fe6ae079b..819f8a587f 100644 --- a/src/MahApps.Metro/Controls/MultiSelectionComboBox/MultiSelectionComboBox.cs +++ b/src/MahApps.Metro/Controls/MultiSelectionComboBox/MultiSelectionComboBox.cs @@ -43,7 +43,27 @@ static MultiSelectionComboBox() public MultiSelectionComboBox() { - this.SetValue(SelectedItemsPropertyKey, new ObservableCollection()); + var collection = new ObservableCollection(); + this.SetValue(SelectedItemsPropertyKey, collection); + } + + /// + /// Raise the SelectionChanged event. + /// + private void InvokeSelectionChanged(IList removedItems, IList addedItems) + { + var selectionChanged = new SelectionChangedEventArgs( + Selector.SelectionChangedEvent, + removedItems, + addedItems) + { + Source = this + }; + + base.OnSelectionChanged(selectionChanged); + + this.UpdateDisplaySelectedItems(); + this.UpdateEditableText(); } #endregion @@ -842,7 +862,7 @@ private void UpdateDisplaySelectedItems(SelectedItemsOrderType selectedItemsOrde private void SelectItemsFromText(int millisecondsToWait) { - if (!this.isUserdefinedTextInputPending || this.isTextChanging) + if (!this.IsEditable || !this.isUserdefinedTextInputPending || this.isTextChanging) { return; } @@ -1114,7 +1134,7 @@ private static void ExecutedClearContentCommand(object sender, ExecutedRoutedEve } } - multiSelectionCombo.ResetEditableText(true); + //multiSelectionCombo.ResetEditableText(true); } } @@ -1186,14 +1206,16 @@ public override void OnApplyTemplate() if (this.PART_PopupListBox is not null) { - this.BeginInvoke(() => - { - this.PART_PopupListBox.SelectionChanged += this.PART_PopupListBox_SelectionChanged; - - this.SyncSelectedItems(this.SelectedItems, this.PART_PopupListBox.SelectedItems, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - - this.PART_PopupListBox.SelectionChanged -= this.PART_PopupListBox_SelectionChanged; - }, DispatcherPriority.DataBind); + this.PART_PopupListBox.SelectionChanged += this.PART_PopupListBox_SelectionChanged; + this.SyncSelectedItems(this.SelectedItems, this.PART_PopupListBox.SelectedItems, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + this.PART_PopupListBox.SelectionChanged -= this.PART_PopupListBox_SelectionChanged; + + //this.BeginInvoke(() => + // { + // this.PART_PopupListBox.SelectionChanged += this.PART_PopupListBox_SelectionChanged; + // this.SyncSelectedItems(this.SelectedItems, this.PART_PopupListBox.SelectedItems, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + // this.PART_PopupListBox.SelectionChanged -= this.PART_PopupListBox_SelectionChanged; + // }, DispatcherPriority.DataBind); } // Do update the text and selection @@ -1837,6 +1859,34 @@ private void SelectedItemsImpl_CollectionChanged(object sender, NotifyCollection #endif { this.SyncSelectedItems(sender as IList, this.PART_PopupListBox?.SelectedItems, e); + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + if (e.NewItems is not null) + { + this.InvokeSelectionChanged(new List(), e.NewItems.Cast().ToList()); + } + + break; + case NotifyCollectionChangedAction.Remove: + if (e.OldItems is not null) + { + this.InvokeSelectionChanged(e.OldItems.Cast().ToList(), new List()); + } + + break; + case NotifyCollectionChangedAction.Reset: + case NotifyCollectionChangedAction.Replace: + if (e.NewItems is not null && e.OldItems is not null) + { + this.InvokeSelectionChanged(e.OldItems.Cast().ToList(), e.NewItems.Cast().ToList()); + } + + break; + case NotifyCollectionChangedAction.Move: + break; // order within SelectedItems doesn't matter + } } private void SyncSelectedItems(IList? sourceCollection, IList? targetCollection, NotifyCollectionChangedEventArgs e) @@ -1953,6 +2003,7 @@ private void PART_SelectedItemsPresenter_MouseLeftButtonUp(object sender, MouseB { // If we have a ScrollViewer (ListBox has) we need to handle this event here as it will not be forwarded to the ToggleButton this.SetCurrentValue(IsDropDownOpenProperty, BooleanBoxes.Box(!this.IsDropDownOpen)); + e.Handled = true; } private void PART_SelectedItemsPresenter_SelectionChanged(object sender, SelectionChangedEventArgs e) diff --git a/src/Mahapps.Metro.Tests/AssemblySetup.cs b/src/Mahapps.Metro.Tests/AssemblySetup.cs new file mode 100644 index 0000000000..7bd697080e --- /dev/null +++ b/src/Mahapps.Metro.Tests/AssemblySetup.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Windows; +using System.Windows.Threading; +using NUnit.Framework; + +namespace MahApps.Metro.Tests +{ + [SetUpFixture] + public class AssemblySetup + { + [OneTimeSetUp] + public void Setup() + { + SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext()); + + var app = new TestApp { ShutdownMode = ShutdownMode.OnExplicitShutdown }; + app.InitializeComponent(); + } + + [OneTimeTearDown] + public void TearDown() + { + Dispatcher.CurrentDispatcher.InvokeShutdown(); + } + } +} \ No newline at end of file diff --git a/src/Mahapps.Metro.Tests/MahApps.Metro.Tests.csproj b/src/Mahapps.Metro.Tests/MahApps.Metro.Tests.csproj index 32209c0777..5e8493a408 100644 --- a/src/Mahapps.Metro.Tests/MahApps.Metro.Tests.csproj +++ b/src/Mahapps.Metro.Tests/MahApps.Metro.Tests.csproj @@ -31,10 +31,12 @@ - - - - + + + + all + runtime; build; native; contentfiles; analyzers + diff --git a/src/Mahapps.Metro.Tests/Properties/AssemblyInfo.cs b/src/Mahapps.Metro.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..9a8f7eb97d --- /dev/null +++ b/src/Mahapps.Metro.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: NUnit.Framework.Apartment(System.Threading.ApartmentState.STA)] \ No newline at end of file diff --git a/src/Mahapps.Metro.Tests/TestApp.xaml b/src/Mahapps.Metro.Tests/TestApp.xaml index 41a8d58fe7..343e976fe1 100644 --- a/src/Mahapps.Metro.Tests/TestApp.xaml +++ b/src/Mahapps.Metro.Tests/TestApp.xaml @@ -5,7 +5,6 @@ - diff --git a/src/Mahapps.Metro.Tests/TestHelpers/ApplicationFixture.cs b/src/Mahapps.Metro.Tests/TestHelpers/ApplicationFixture.cs deleted file mode 100644 index 9947243c98..0000000000 --- a/src/Mahapps.Metro.Tests/TestHelpers/ApplicationFixture.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Windows; -using System.Windows.Threading; -using Xunit; - -namespace MahApps.Metro.Tests.TestHelpers -{ - public class ApplicationFixture : IDisposable - { - public ApplicationFixture() - { - // ... initialize - TestHost.Initialize(); - } - - public void Dispose() - { - // ... clean up - GC.Collect(); - Dispatcher.ExitAllFrames(); - Application.Current.Dispatcher.Invoke(Application.Current.Shutdown); - } - } - - [CollectionDefinition("ApplicationFixtureCollection")] - public class ApplicationFixtureCollectionClass : ICollectionFixture - { - } -} \ No newline at end of file diff --git a/src/Mahapps.Metro.Tests/TestHelpers/AutomationTestBase.cs b/src/Mahapps.Metro.Tests/TestHelpers/AutomationTestBase.cs deleted file mode 100644 index d1065d475d..0000000000 --- a/src/Mahapps.Metro.Tests/TestHelpers/AutomationTestBase.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Windows; -using ControlzEx.Theming; -using MahApps.Metro.Controls; -using Xunit; - -namespace MahApps.Metro.Tests.TestHelpers -{ - /// - /// This is the base class for all of our UI tests. - /// - [Collection("ApplicationFixtureCollection")] - public class AutomationTestBase : IDisposable - { - public AutomationTestBase() - { - var message = $"Create test class '{this.GetType().Name}' with Thread.CurrentThread: {Thread.CurrentThread.ManagedThreadId}" + - $" and Current.Dispatcher.Thread: {Application.Current.Dispatcher.Thread.ManagedThreadId}"; - Debug.WriteLine(message); - - // Reset the application as good as we can - Application.Current.Invoke(() => - { - var windows = Application.Current.Windows.OfType().ToList(); - foreach (Window window in windows) - { - window.Close(); - } - }); - - Application.Current.Invoke(() => { ThemeManager.Current.ChangeTheme(Application.Current, "Light.Blue"); }); - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public virtual void Dispose() - { - var message = $"Dispose test class '{this.GetType().Name}' with Thread.CurrentThread: {Thread.CurrentThread.ManagedThreadId}" + - $" and Current.Dispatcher.Thread: {Application.Current.Dispatcher.Thread.ManagedThreadId}"; - Debug.WriteLine(message); - } - } -} \ No newline at end of file diff --git a/src/Mahapps.Metro.Tests/TestHelpers/AutomationTestFixtureBase.cs b/src/Mahapps.Metro.Tests/TestHelpers/AutomationTestFixtureBase.cs deleted file mode 100644 index 33002866f6..0000000000 --- a/src/Mahapps.Metro.Tests/TestHelpers/AutomationTestFixtureBase.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Xunit; - -namespace MahApps.Metro.Tests.TestHelpers -{ - public class AutomationTestFixtureBase : AutomationTestBase, IClassFixture - where TFixture : class - { - protected readonly TFixture fixture; - - public AutomationTestFixtureBase(TFixture fixture) - : base() - { - this.fixture = fixture; - } - } -} \ No newline at end of file diff --git a/src/Mahapps.Metro.Tests/TestHelpers/DisplayTestMethodNameAttribute.cs b/src/Mahapps.Metro.Tests/TestHelpers/DisplayTestMethodNameAttribute.cs deleted file mode 100644 index f02bc5cf5c..0000000000 --- a/src/Mahapps.Metro.Tests/TestHelpers/DisplayTestMethodNameAttribute.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Diagnostics; -using System.Reflection; -using System.Threading; -using System.Windows; -using Xunit.Sdk; - -namespace MahApps.Metro.Tests.TestHelpers -{ - public class DisplayTestMethodNameAttribute : BeforeAfterTestAttribute - { - public override void Before(MethodInfo methodUnderTest) - { - var message = $"Setup for test '{methodUnderTest.Name}' with Thread.CurrentThread: {Thread.CurrentThread.ManagedThreadId}" + - $" and Current.Dispatcher.Thread: {Application.Current.Dispatcher.Thread.ManagedThreadId}"; - Debug.WriteLine(message); - } - - public override void After(MethodInfo methodUnderTest) - { - var message = $"TearDown for test '{methodUnderTest.Name}' with Thread.CurrentThread: {Thread.CurrentThread.ManagedThreadId}" + - $" and Current.Dispatcher.Thread: {Application.Current.Dispatcher.Thread.ManagedThreadId}"; - Debug.WriteLine(message); - } - } -} \ No newline at end of file diff --git a/src/Mahapps.Metro.Tests/TestHelpers/SwitchContextToUiThreadAwaiter.cs b/src/Mahapps.Metro.Tests/TestHelpers/SwitchContextToUiThreadAwaiter.cs deleted file mode 100644 index 636872b3c7..0000000000 --- a/src/Mahapps.Metro.Tests/TestHelpers/SwitchContextToUiThreadAwaiter.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Runtime.CompilerServices; -using System.Windows.Threading; - -namespace MahApps.Metro.Tests.TestHelpers -{ - public class SwitchContextToUiThreadAwaiter : INotifyCompletion - { - private readonly Dispatcher uiContext; - - public SwitchContextToUiThreadAwaiter(Dispatcher uiContext) - { - this.uiContext = uiContext; - } - - public SwitchContextToUiThreadAwaiter GetAwaiter() - { - return this; - } - - public bool IsCompleted => false; - - public void OnCompleted(Action continuation) - { - this.uiContext.Invoke(new Action(continuation)); - } - - public void GetResult() - { - } - } -} \ No newline at end of file diff --git a/src/Mahapps.Metro.Tests/TestHelpers/TestHost.cs b/src/Mahapps.Metro.Tests/TestHelpers/TestHost.cs deleted file mode 100644 index bde772580d..0000000000 --- a/src/Mahapps.Metro.Tests/TestHelpers/TestHost.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; - -namespace MahApps.Metro.Tests.TestHelpers -{ - /// - /// This class is the ultimate hack to work around that we can't - /// create more than one application in the same AppDomain - /// - /// It is initialized once at startup and is never properly cleaned up, - /// this means the AppDomain will throw an exception when xUnit unloads it. - /// - /// Your test runner will inevitably hate you and hang endlessly after every test has run. - /// The Resharper runner will also throw an exception message in your face. - /// - /// Better than no unit tests. - /// - public class TestHost - { - private TestApp? app; - private readonly Thread? appThread; - private readonly AutoResetEvent gate = new(false); - - private static TestHost? testHost; - - public static void Initialize() - { - testHost ??= new TestHost(); - } - - private TestHost() - { - this.appThread = new Thread(this.StartDispatcher); - this.appThread.SetApartmentState(ApartmentState.STA); - this.appThread.Start(); - - this.gate.WaitOne(); - } - - private void StartDispatcher() - { - this.app = new TestApp { ShutdownMode = ShutdownMode.OnExplicitShutdown }; - this.app.InitializeComponent(); - this.app.Exit += (_, _) => - { - var message = $"Exit TestApp with Thread.CurrentThread: {Thread.CurrentThread.ManagedThreadId}" + - $" and Current.Dispatcher.Thread: {Application.Current.Dispatcher.Thread.ManagedThreadId}"; - Debug.WriteLine(message); - }; - this.app.Startup += async (_, _) => - { - var message = $"Start TestApp with Thread.CurrentThread: {Thread.CurrentThread.ManagedThreadId}" + - $" and Current.Dispatcher.Thread: {Application.Current.Dispatcher.Thread.ManagedThreadId}"; - Debug.WriteLine(message); - this.gate.Set(); - await Task.Yield(); - }; - this.app.Run(); - } - - /// - /// Await this method in every test that should run on the UI thread. - /// - public static SwitchContextToUiThreadAwaiter SwitchToAppThread() - { - if (testHost?.app is null) - { - throw new InvalidOperationException($"{nameof(TestHost)} is not initialized!"); - } - - return new SwitchContextToUiThreadAwaiter(testHost.app.Dispatcher); - } - } -} \ No newline at end of file diff --git a/src/Mahapps.Metro.Tests/TestHelpers/WindowHelpers.cs b/src/Mahapps.Metro.Tests/TestHelpers/WindowHelpers.cs index 67dc7d148a..d46c48d681 100644 --- a/src/Mahapps.Metro.Tests/TestHelpers/WindowHelpers.cs +++ b/src/Mahapps.Metro.Tests/TestHelpers/WindowHelpers.cs @@ -3,13 +3,14 @@ // See the LICENSE file in the project root for more information. using System; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using MahApps.Metro.Controls; -using Xunit; +using NUnit.Framework; namespace MahApps.Metro.Tests.TestHelpers { @@ -22,10 +23,19 @@ public static Task CreateInvisibleWindowAsync(Action? onLoadedAction = var window = new T { - Visibility = Visibility.Hidden, + Width = 800, + Height = 600, ShowInTaskbar = false }; + if (Debugger.IsAttached == false) + { + window.Left = int.MinValue; + window.Top = int.MinValue; + } + + window.SetCurrentValue(FrameworkElement.UseLayoutRoundingProperty, true); + void OnLoaded(object sender, RoutedEventArgs e) { window.Loaded -= OnLoaded; @@ -49,21 +59,21 @@ void OnActivated(object sender, EventArgs args) public static void AssertWindowCommandsColor(this MetroWindow window, Color color) { - Assert.NotNull(window.RightWindowCommands); + Assert.That(window.RightWindowCommands, Is.Not.Null); foreach (var element in window.RightWindowCommands!.Items.OfType