diff --git a/src/MahApps.Metro.Samples/MahApps.Metro.Demo/ExampleViews/MultiSelectionComboBoxExample.xaml b/src/MahApps.Metro.Samples/MahApps.Metro.Demo/ExampleViews/MultiSelectionComboBoxExample.xaml new file mode 100644 index 0000000000..68405a5b8d --- /dev/null +++ b/src/MahApps.Metro.Samples/MahApps.Metro.Demo/ExampleViews/MultiSelectionComboBoxExample.xaml @@ -0,0 +1,305 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MahApps.Metro.Samples/MahApps.Metro.Demo/ExampleViews/MultiSelectionComboBoxExample.xaml.cs b/src/MahApps.Metro.Samples/MahApps.Metro.Demo/ExampleViews/MultiSelectionComboBoxExample.xaml.cs new file mode 100644 index 0000000000..2407fee394 --- /dev/null +++ b/src/MahApps.Metro.Samples/MahApps.Metro.Demo/ExampleViews/MultiSelectionComboBoxExample.xaml.cs @@ -0,0 +1,52 @@ +// 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 MahApps.Metro.Controls; +using MahApps.Metro.Controls.Dialogs; +using System.Diagnostics; +using System.Windows.Controls; + +namespace MetroDemo.ExampleViews +{ + /// + /// Interaction logic for MultiSelectionComboBoxExample.xaml + /// + public partial class MultiSelectionComboBoxExample : UserControl + { + public MultiSelectionComboBoxExample() + { + this.InitializeComponent(); + } + + private void Mscb_Example_AddingItem(object? sender, AddingItemEventArgs args) + { + // We don't want to get double entries so let`s check if we already have one. + args.Accepted = args.TargetList is not null && !args.TargetList.Contains(args.ParsedObject); + } + + private async void Mscb_Example_AddedItem(object? sender, AddedItemEventArgs args) + { + var window = this.TryFindParent(); + if (window is null) + { + return; + } + + await window.ShowMessageAsync("Added Item", $"Successfully added \"{args.AddedItem}\" to your Items-Collection"); + } + + private void mscb_Example_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + foreach (var item in e.AddedItems) + { + Debug.WriteLine($"MultiSelectionComboBox-Example: Selected item \"{item}\""); + } + + foreach (var item in e.RemovedItems) + { + Debug.WriteLine($"MultiSelectionComboBox-Example: Unselected item \"{item}\""); + } + } + } +} \ No newline at end of file diff --git a/src/MahApps.Metro.Samples/MahApps.Metro.Demo/MainWindow.xaml b/src/MahApps.Metro.Samples/MahApps.Metro.Demo/MainWindow.xaml index a524d41798..e381e887f0 100644 --- a/src/MahApps.Metro.Samples/MahApps.Metro.Demo/MainWindow.xaml +++ b/src/MahApps.Metro.Samples/MahApps.Metro.Demo/MainWindow.xaml @@ -332,6 +332,10 @@ + + + + (x => x is not null && x.CanUseToggleSwitch, async x => { await this._dialogCoordinator.ShowMessageAsync(this, "ToggleSwitch", "The ToggleSwitch is now Off."); }); + + this.MyObjectParser = new ObjectParser(this, this._dialogCoordinator); } public ICommand ArtistsDropDownCommand { get; } @@ -551,5 +553,168 @@ private void ToggleIconScaling(MultiFrameImageMode? multiFrameImageMode) public bool IsNoScaleSmallerFrame => ((MetroWindow)Application.Current.MainWindow).IconScalingMode == MultiFrameImageMode.NoScaleSmallerFrame; public bool IsToggleSwitchVisible { get; set; } + + public ObservableCollection Animals { get; } = new() + { + "African elephant", + "Ant", + "Antelope", + "Aphid", + "Arctic wolf", + "Badger", + "Bald eagle", + "Bat", + "Bear", + "Bee", + "Beetle", + "Bengal tiger", + "Bison", + "Butterfly", + "Camel", + "Cat", + "Caterpillar", + "Chicken", + "Chimpanzee", + "Chipmunk", + "Cicada", + "Clam", + "Cockroach", + "Cormorant", + "Cow", + "Coyote", + "Crab", + "Crow", + "Cuckoo", + "Deer", + "Dog", + "Dolphin", + "Donkey", + "Dove", + "Dragonfly", + "Duck", + "Elephant", + "Elk", + "Finch", + "Fish", + "Flamingo", + "Flea", + "Fly", + "Fox", + "Frigatebird", + "Giraffe", + "Goat", + "Goldfish", + "Goose", + "Gorilla", + "Grasshopper", + "Great horned owl", + "Guinea pig", + "Hamster", + "Hare", + "Hawk", + "Hedgehog", + "Hippopotamus", + "Hornbill", + "Horse", + "Horse-fly", + "Howler monkey", + "Hummingbird", + "Hyena", + "Ibis", + "Jackal", + "Jellyfish", + "Kangaroo", + "Koala", + "Ladybugs(NAmE) /ladybirds(BrE)", + "Leopard", + "Lion", + "Lizard", + "Lobster", + "Lynxes", + "Mantis", + "Marten", + "Mole", + "Monkey", + "Mosquito", + "Moth", + "Mouse", + "Octopus", + "Okapi", + "Orangutan", + "Otter", + "Owl", + "Ox", + "Oyster", + "Panda", + "Parrot", + "Pelecaniformes", + "Pelican", + "Penguin", + "Pig", + "Pigeon", + "Porcupine", + "Possum", + "Puma", + "Rabbit", + "Raccoon", + "Rat", + "Raven", + "Red dear", + "Red panda", + "Red squirrel", + "Reindeer", + "Rhinoceros", + "Robin", + "Sandpiper", + "Sea turtle", + "Seahorse", + "Seal", + "Shark", + "Sheep", + "Shell", + "Shrimp", + "Snake", + "Sparrow", + "Squid", + "Squirrel", + "Squirrel monkey", + "Starfish", + "Stork", + "Swallow", + "Swan", + "Termite", + "Tern", + "Tick", + "Tiger", + "Turkey", + "Turtle", + "Walrus", + "Wasp", + "Whale", + "Whitefly", + "Wild boar", + "Wolf", + "Wombat", + "Woodpecker", + "Zebra" + }; + + public ObservableCollection SelectedAnimals { get; } = new() + { + "Dog", + "Cat", + "Zebra" + }; + + private object? myFavoriteAnimal; + + [Display(Prompt = "Select your favorite animal(s)")] + public object? MyFavoriteAnimal + { + get => this.myFavoriteAnimal; + set => this.Set(ref this.myFavoriteAnimal, value); + } + + public ObjectParser? MyObjectParser { get; } } } \ No newline at end of file diff --git a/src/MahApps.Metro.Samples/MahApps.Metro.Demo/Models/Album.cs b/src/MahApps.Metro.Samples/MahApps.Metro.Demo/Models/Album.cs index 44c1029908..d1d28cbbc2 100644 --- a/src/MahApps.Metro.Samples/MahApps.Metro.Demo/Models/Album.cs +++ b/src/MahApps.Metro.Samples/MahApps.Metro.Demo/Models/Album.cs @@ -73,6 +73,12 @@ public virtual Artist? Artist get => this._artist; set => this.Set(ref this._artist, value); } + + public override string ToString() + { + return $"{this.Artist}: {this.Title} ({this.Price.ToString("C")})"; + } + } public static class SampleData diff --git a/src/MahApps.Metro.Samples/MahApps.Metro.Demo/Models/Artist.cs b/src/MahApps.Metro.Samples/MahApps.Metro.Demo/Models/Artist.cs index 5270392f8b..2a388031d7 100644 --- a/src/MahApps.Metro.Samples/MahApps.Metro.Demo/Models/Artist.cs +++ b/src/MahApps.Metro.Samples/MahApps.Metro.Demo/Models/Artist.cs @@ -30,5 +30,14 @@ public List? Albums get => this._albums; set => this.Set(ref this._albums, value); } + +#if NET5_0_OR_GREATER || NETCOREAPP + public override string? ToString() +#else + public override string ToString() +#endif + { + return this.Name ?? base.ToString(); + } } } \ No newline at end of file diff --git a/src/MahApps.Metro.Samples/MahApps.Metro.Demo/Models/Genre.cs b/src/MahApps.Metro.Samples/MahApps.Metro.Demo/Models/Genre.cs index badca7b796..a35ba9d05f 100644 --- a/src/MahApps.Metro.Samples/MahApps.Metro.Demo/Models/Genre.cs +++ b/src/MahApps.Metro.Samples/MahApps.Metro.Demo/Models/Genre.cs @@ -37,5 +37,14 @@ public List? Albums get => this._albums; set => this.Set(ref this._albums, value); } + +#if NET5_0_OR_GREATER || NETCOREAPP + public override string? ToString() +#else + public override string ToString() +#endif + { + return this.Name ?? base.ToString(); + } } } \ No newline at end of file diff --git a/src/MahApps.Metro.Samples/MahApps.Metro.Demo/Models/ObjectParser.cs b/src/MahApps.Metro.Samples/MahApps.Metro.Demo/Models/ObjectParser.cs new file mode 100644 index 0000000000..0cfcbabf4c --- /dev/null +++ b/src/MahApps.Metro.Samples/MahApps.Metro.Demo/Models/ObjectParser.cs @@ -0,0 +1,55 @@ +// 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 MahApps.Metro.Controls; +using MahApps.Metro.Controls.Dialogs; +using System; +using System.Globalization; + +namespace MetroDemo.Models +{ + public class ObjectParser : IParseStringToObject + { + private readonly MainWindowViewModel _mainWindowViewModel; + private readonly IDialogCoordinator _dialogCoordinator; + + public ObjectParser(MainWindowViewModel mainWindowViewModel, IDialogCoordinator dialogCoordinator) + { + this._mainWindowViewModel = mainWindowViewModel; + this._dialogCoordinator = dialogCoordinator; + } + + /// + public bool TryCreateObjectFromString(string? input, + out object? result, + CultureInfo? culture = null, + string? stringFormat = null, + Type? elementType = null) + { + if (string.IsNullOrWhiteSpace(input)) + { + result = null; + return false; + } + + MetroDialogSettings dialogSettings = new MetroDialogSettings + { + AffirmativeButtonText = "Yes", + NegativeButtonText = "No", + DefaultButtonFocus = MessageDialogResult.Affirmative + }; + + if (this._dialogCoordinator.ShowModalMessageExternal(this._mainWindowViewModel, "Add Animal", $"Do you want to add \"{input}\" to the animals list?", MessageDialogStyle.AffirmativeAndNegative, dialogSettings) == MessageDialogResult.Affirmative) + { + result = input!.Trim(); + return true; + } + else + { + result = null; + return false; + } + } + } +} \ No newline at end of file diff --git a/src/MahApps.Metro/Controls/Helper/BindingHelper.cs b/src/MahApps.Metro/Controls/Helper/BindingHelper.cs new file mode 100644 index 0000000000..c4c8c62a3b --- /dev/null +++ b/src/MahApps.Metro/Controls/Helper/BindingHelper.cs @@ -0,0 +1,129 @@ +// 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.Data; + +namespace MahApps.Metro.Controls.Helper +{ + /// + /// A helper class to evaluate Bindings in code behind + /// + public static class BindingHelper + { + /// + /// A dummy property to initialize the binding to evaluate + /// + private static readonly DependencyProperty DummyProperty + = DependencyProperty.RegisterAttached("Dummy", + typeof(object), + typeof(BindingHelper), + new UIPropertyMetadata(null)); + + /// + /// A dummy property to initialize the binding to evaluate. This property supports also string format. + /// + private static readonly DependencyProperty DummyTextProperty + = DependencyProperty.RegisterAttached("DummyText", + typeof(string), + typeof(BindingHelper), + new UIPropertyMetadata(null)); + + /// + /// Evaluates a defined -path on the given object + /// + /// the object to evaluate + /// the binding expression to evaluate + /// the result of the + public static object? Eval(object? source, string? expression) + { + Binding binding = new Binding(expression) { Source = source }; + return Eval(binding); + } + + /// + /// Evaluates a defined -path on the given object + /// + /// the object to evaluate + /// the binding expression to evaluate + /// the string format to use + /// the result of the + public static object? Eval(object? source, string? expression, string? format) + { + Binding binding = new Binding(expression) { Source = source, StringFormat = format }; + return Eval(binding); + } + + /// + /// Evaluates a defined on the given object + /// + /// The to evaluate + /// the object to evaluate + /// + public static object? Eval(Binding? binding, object? source) + { + if (binding is null) + { + throw new ArgumentNullException(nameof(binding)); + } + + Binding newBinding = new Binding + { + Source = source, + AsyncState = binding.AsyncState, + BindingGroupName = binding.BindingGroupName, + BindsDirectlyToSource = binding.BindsDirectlyToSource, + Path = binding.Path, + Converter = binding.Converter, + ConverterCulture = binding.ConverterCulture, + ConverterParameter = binding.ConverterParameter, + FallbackValue = binding.FallbackValue, + IsAsync = binding.IsAsync, + Mode = BindingMode.OneWay, + StringFormat = binding.StringFormat, + TargetNullValue = binding.TargetNullValue + }; + + return Eval(newBinding); + } + + /// + /// Evaluates a defined on the given + /// + /// The to evaluate + /// optional: The to evaluate + /// The resulting object + public static object? Eval(Binding? binding, DependencyObject? dependencyObject) + { + if (binding is null) + { + throw new ArgumentNullException(nameof(binding)); + } + + dependencyObject ??= new DependencyObject(); + + if (string.IsNullOrEmpty(binding.StringFormat)) + { + BindingOperations.SetBinding(dependencyObject, DummyProperty, binding); + return dependencyObject.GetValue(DummyProperty); + } + else + { + BindingOperations.SetBinding(dependencyObject, DummyTextProperty, binding); + return dependencyObject.GetValue(DummyTextProperty); + } + } + + /// + /// Evaluates a defined on the given + /// + /// The to evaluate + /// The resulting object + public static object? Eval(Binding? binding) + { + return Eval(binding, null); + } + } +} \ No newline at end of file diff --git a/src/MahApps.Metro/Controls/Helper/ComboBoxHelper.cs b/src/MahApps.Metro/Controls/Helper/ComboBoxHelper.cs index 42988cf369..e74b499421 100644 --- a/src/MahApps.Metro/Controls/Helper/ComboBoxHelper.cs +++ b/src/MahApps.Metro/Controls/Helper/ComboBoxHelper.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using MahApps.Metro.ValueBoxes; using System.ComponentModel; using System.Windows; using System.Windows.Controls; +using System.Windows.Input; namespace MahApps.Metro.Controls { @@ -66,5 +68,57 @@ public static void SetCharacterCasing(UIElement obj, CharacterCasing value) { obj.SetValue(CharacterCasingProperty, value); } + + + /// Identifies the InterceptMouseWheelSelection attached dependcy property. + public static readonly DependencyProperty InterceptMouseWheelSelectionProperty = DependencyProperty.RegisterAttached("InterceptMouseWheelSelection", typeof(bool), typeof(ComboBoxHelper), new PropertyMetadata(BooleanBoxes.TrueBox, OnInterceptMouseWheelSelectionPropertyChangedCallback)); + + + /// + /// Gets if the given accepts selection via mouse wheel. + /// + [Category(AppName.MahApps)] + [AttachedPropertyBrowsableForType(typeof(ComboBox))] + public static bool GetInterceptMouseWheelSelection(DependencyObject obj) + { + return (bool)obj.GetValue(InterceptMouseWheelSelectionProperty); + } + + /// + /// Sets if the given accepts selection via mouse wheel. The default is true. + /// + [Category(AppName.MahApps)] + [AttachedPropertyBrowsableForType(typeof(ComboBox))] + public static void SetInterceptMouseWheelSelection(ComboBox obj, bool value) + { + obj.SetValue(InterceptMouseWheelSelectionProperty, BooleanBoxes.Box(value)); + } + + private static void OnInterceptMouseWheelSelectionPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ComboBox comboBox && e.NewValue != e.OldValue && e.NewValue is bool) + { + comboBox.PreviewMouseWheel -= ComboBox_PreviewMouseWheel; + + // If this property is set to false we need to handle the MouseWheel before the ComboBox does. + if (!((bool)e.NewValue)) + { + comboBox.PreviewMouseWheel += ComboBox_PreviewMouseWheel; + } + } + } + + private static void ComboBox_PreviewMouseWheel(object sender, MouseWheelEventArgs e) + { + if (sender is MultiSelectionComboBox) + { + // This will be handled by MultiSelectionComboBox directly so we don't need to do anything here. + } + else if (sender is ComboBox comboBox) + { + // mark the event as handled to cancel selection. We should not handle it if the drop down is open. + e.Handled = !comboBox.IsDropDownOpen; + } + } } } \ No newline at end of file diff --git a/src/MahApps.Metro/Controls/Helper/MultiSelectorHelper.cs b/src/MahApps.Metro/Controls/Helper/MultiSelectorHelper.cs index 11b753d6f9..06d6395ef2 100644 --- a/src/MahApps.Metro/Controls/Helper/MultiSelectorHelper.cs +++ b/src/MahApps.Metro/Controls/Helper/MultiSelectorHelper.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -30,9 +30,9 @@ public static readonly DependencyProperty SelectedItemsProperty /// private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - if (!(d is ListBox || d is MultiSelector)) + if (!(d is ListBox || d is MultiSelector || d is MultiSelectionComboBox)) { - throw new ArgumentException("The property 'SelectedItems' may only be set on ListBox or MultiSelector elements."); + throw new ArgumentException("The property 'SelectedItems' may only be set on ListBox, MultiSelector or MultiSelectionComboBox elements."); } if (e.OldValue != e.NewValue) @@ -135,6 +135,17 @@ public MultiSelectorBinding(Selector selector, IList collection) multiSelector.SelectedItems.Add(newItem); } } + else if (selector is MultiSelectionComboBox multiSelectionComboBox) + { + if (multiSelectionComboBox.SelectedItems is not null) + { + multiSelectionComboBox.SelectedItems.Clear(); + foreach (var newItem in collection) + { + multiSelectionComboBox.SelectedItems.Add(newItem); + } + } + } } /// diff --git a/src/MahApps.Metro/Controls/Helper/TextBoxHelper.cs b/src/MahApps.Metro/Controls/Helper/TextBoxHelper.cs index 91f4a10e2f..544ee83822 100644 --- a/src/MahApps.Metro/Controls/Helper/TextBoxHelper.cs +++ b/src/MahApps.Metro/Controls/Helper/TextBoxHelper.cs @@ -77,6 +77,7 @@ public static readonly DependencyProperty WatermarkProperty [AttachedPropertyBrowsableForType(typeof(TextBoxBase))] [AttachedPropertyBrowsableForType(typeof(PasswordBox))] [AttachedPropertyBrowsableForType(typeof(ComboBox))] + [AttachedPropertyBrowsableForType(typeof(MultiSelectionComboBox))] [AttachedPropertyBrowsableForType(typeof(DatePicker))] [AttachedPropertyBrowsableForType(typeof(TimePickerBase))] [AttachedPropertyBrowsableForType(typeof(NumericUpDown))] @@ -91,6 +92,7 @@ public static string GetWatermark(DependencyObject obj) [AttachedPropertyBrowsableForType(typeof(TextBoxBase))] [AttachedPropertyBrowsableForType(typeof(PasswordBox))] [AttachedPropertyBrowsableForType(typeof(ComboBox))] + [AttachedPropertyBrowsableForType(typeof(MultiSelectionComboBox))] [AttachedPropertyBrowsableForType(typeof(DatePicker))] [AttachedPropertyBrowsableForType(typeof(TimePickerBase))] [AttachedPropertyBrowsableForType(typeof(NumericUpDown))] @@ -118,6 +120,7 @@ public static readonly DependencyProperty WatermarkAlignmentProperty [AttachedPropertyBrowsableForType(typeof(TextBoxBase))] [AttachedPropertyBrowsableForType(typeof(PasswordBox))] [AttachedPropertyBrowsableForType(typeof(ComboBox))] + [AttachedPropertyBrowsableForType(typeof(MultiSelectionComboBox))] [AttachedPropertyBrowsableForType(typeof(DatePicker))] [AttachedPropertyBrowsableForType(typeof(TimePickerBase))] [AttachedPropertyBrowsableForType(typeof(NumericUpDown))] @@ -135,6 +138,7 @@ public static TextAlignment GetWatermarkAlignment(DependencyObject obj) [AttachedPropertyBrowsableForType(typeof(TextBoxBase))] [AttachedPropertyBrowsableForType(typeof(PasswordBox))] [AttachedPropertyBrowsableForType(typeof(ComboBox))] + [AttachedPropertyBrowsableForType(typeof(MultiSelectionComboBox))] [AttachedPropertyBrowsableForType(typeof(DatePicker))] [AttachedPropertyBrowsableForType(typeof(TimePickerBase))] [AttachedPropertyBrowsableForType(typeof(NumericUpDown))] @@ -162,6 +166,7 @@ public static readonly DependencyProperty WatermarkTrimmingProperty [AttachedPropertyBrowsableForType(typeof(TextBoxBase))] [AttachedPropertyBrowsableForType(typeof(PasswordBox))] [AttachedPropertyBrowsableForType(typeof(ComboBox))] + [AttachedPropertyBrowsableForType(typeof(MultiSelectionComboBox))] [AttachedPropertyBrowsableForType(typeof(DatePicker))] [AttachedPropertyBrowsableForType(typeof(TimePickerBase))] [AttachedPropertyBrowsableForType(typeof(NumericUpDown))] @@ -179,6 +184,7 @@ public static TextTrimming GetWatermarkTrimming(DependencyObject obj) [AttachedPropertyBrowsableForType(typeof(TextBoxBase))] [AttachedPropertyBrowsableForType(typeof(PasswordBox))] [AttachedPropertyBrowsableForType(typeof(ComboBox))] + [AttachedPropertyBrowsableForType(typeof(MultiSelectionComboBox))] [AttachedPropertyBrowsableForType(typeof(DatePicker))] [AttachedPropertyBrowsableForType(typeof(TimePickerBase))] [AttachedPropertyBrowsableForType(typeof(NumericUpDown))] @@ -229,6 +235,7 @@ public static readonly DependencyProperty UseFloatingWatermarkProperty [AttachedPropertyBrowsableForType(typeof(TextBoxBase))] [AttachedPropertyBrowsableForType(typeof(PasswordBox))] [AttachedPropertyBrowsableForType(typeof(ComboBox))] + [AttachedPropertyBrowsableForType(typeof(MultiSelectionComboBox))] [AttachedPropertyBrowsableForType(typeof(NumericUpDown))] [AttachedPropertyBrowsableForType(typeof(HotKeyBox))] [AttachedPropertyBrowsableForType(typeof(ColorPicker))] @@ -241,6 +248,7 @@ public static bool GetUseFloatingWatermark(DependencyObject obj) [AttachedPropertyBrowsableForType(typeof(TextBoxBase))] [AttachedPropertyBrowsableForType(typeof(PasswordBox))] [AttachedPropertyBrowsableForType(typeof(ComboBox))] + [AttachedPropertyBrowsableForType(typeof(MultiSelectionComboBox))] [AttachedPropertyBrowsableForType(typeof(NumericUpDown))] [AttachedPropertyBrowsableForType(typeof(HotKeyBox))] [AttachedPropertyBrowsableForType(typeof(ColorPicker))] @@ -328,6 +336,7 @@ private static void OnControlWithAutoWatermarkSupportLoaded(object o, RoutedEven [Category(AppName.MahApps)] [AttachedPropertyBrowsableForType(typeof(TextBox))] [AttachedPropertyBrowsableForType(typeof(ComboBox))] + [AttachedPropertyBrowsableForType(typeof(MultiSelectionComboBox))] [AttachedPropertyBrowsableForType(typeof(DatePicker))] [AttachedPropertyBrowsableForType(typeof(TimePickerBase))] [AttachedPropertyBrowsableForType(typeof(NumericUpDown))] @@ -355,6 +364,7 @@ public static bool GetAutoWatermark(DependencyObject element) [Category(AppName.MahApps)] [AttachedPropertyBrowsableForType(typeof(TextBox))] [AttachedPropertyBrowsableForType(typeof(ComboBox))] + [AttachedPropertyBrowsableForType(typeof(MultiSelectionComboBox))] [AttachedPropertyBrowsableForType(typeof(DatePicker))] [AttachedPropertyBrowsableForType(typeof(TimePickerBase))] [AttachedPropertyBrowsableForType(typeof(NumericUpDown))] @@ -376,7 +386,8 @@ private static readonly Dictionary AutoWatermarkProper { typeof(TimePicker), TimePickerBase.SelectedDateTimeProperty }, { typeof(DateTimePicker), TimePickerBase.SelectedDateTimeProperty }, { typeof(ColorPicker), ColorPickerBase.SelectedColorProperty }, - { typeof(ColorCanvas), ColorPickerBase.SelectedColorProperty } + { typeof(ColorCanvas), ColorPickerBase.SelectedColorProperty }, + { typeof(MultiSelectionComboBox), MultiSelectionComboBox.SelectedItemProperty } }; internal static readonly DependencyPropertyKey TextLengthPropertyKey @@ -408,6 +419,7 @@ public static readonly DependencyProperty HasTextProperty [AttachedPropertyBrowsableForType(typeof(TextBoxBase))] [AttachedPropertyBrowsableForType(typeof(PasswordBox))] [AttachedPropertyBrowsableForType(typeof(ComboBox))] + [AttachedPropertyBrowsableForType(typeof(MultiSelectionComboBox))] [AttachedPropertyBrowsableForType(typeof(DatePicker))] [AttachedPropertyBrowsableForType(typeof(NumericUpDown))] public static bool GetHasText(DependencyObject obj) @@ -419,6 +431,7 @@ public static bool GetHasText(DependencyObject obj) [AttachedPropertyBrowsableForType(typeof(TextBoxBase))] [AttachedPropertyBrowsableForType(typeof(PasswordBox))] [AttachedPropertyBrowsableForType(typeof(ComboBox))] + [AttachedPropertyBrowsableForType(typeof(MultiSelectionComboBox))] [AttachedPropertyBrowsableForType(typeof(DatePicker))] [AttachedPropertyBrowsableForType(typeof(NumericUpDown))] public static void SetHasText(DependencyObject obj, bool value) @@ -1050,7 +1063,7 @@ public static void ButtonClicked(object sender, RoutedEventArgs e) { var button = (Button)sender; - var parent = button.GetAncestors().FirstOrDefault(a => a is RichTextBox || a is TextBox || a is PasswordBox || a is ComboBox || a is ColorPickerBase); + var parent = button.GetAncestors().FirstOrDefault(a => a is RichTextBox || a is TextBox || a is PasswordBox || a is ComboBox || a is ColorPickerBase || a is MultiSelectionComboBox); if (parent is null) { return; @@ -1085,6 +1098,29 @@ public static void ButtonClicked(object sender, RoutedEventArgs e) passwordBox.Clear(); passwordBox.GetBindingExpression(PasswordBoxBindingBehavior.PasswordProperty)?.UpdateSource(); } + else if (parent is MultiSelectionComboBox multiSelectionComboBox) + { + if (multiSelectionComboBox.HasCustomText) + { + multiSelectionComboBox.ResetEditableText(true); + } + else + { + switch (multiSelectionComboBox.SelectionMode) + { + case SelectionMode.Single: + multiSelectionComboBox.SetCurrentValue(MultiSelectionComboBox.SelectedItemProperty, null); + break; + case SelectionMode.Multiple: + case SelectionMode.Extended: + multiSelectionComboBox.SelectedItems?.Clear(); + break; + default: + throw new NotSupportedException("Unknown SelectionMode"); + } + multiSelectionComboBox.ResetEditableText(true); + } + } else if (parent is ComboBox comboBox) { if (comboBox.IsEditable) diff --git a/src/MahApps.Metro/Controls/MultiSelectionComboBox/AddedItemEventArgs.cs b/src/MahApps.Metro/Controls/MultiSelectionComboBox/AddedItemEventArgs.cs new file mode 100644 index 0000000000..b817ee55e0 --- /dev/null +++ b/src/MahApps.Metro/Controls/MultiSelectionComboBox/AddedItemEventArgs.cs @@ -0,0 +1,47 @@ +// 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.Collections; +using System.Windows; + +namespace MahApps.Metro.Controls +{ + /// + /// Provides data for the + /// + public class AddedItemEventArgs : RoutedEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The AddedItemEvent + /// The source object + /// The added object + /// The target where the should be added + public AddedItemEventArgs(RoutedEvent routedEvent, + object source, + object? addedItem, + IList? targetList) + : base(routedEvent, source) + { + this.AddedItem = addedItem; + this.TargetList = targetList; + } + + /// + /// Gets the added item + /// + public object? AddedItem { get; } + + /// + /// Gets the where the was added to + /// + public IList? TargetList { get; } + } + + /// + /// RoutedEventHandler used for the . + /// + public delegate void AddedItemEventArgsHandler(object? sender, AddedItemEventArgs args); +} \ No newline at end of file diff --git a/src/MahApps.Metro/Controls/MultiSelectionComboBox/AddingItemEventArgs.cs b/src/MahApps.Metro/Controls/MultiSelectionComboBox/AddingItemEventArgs.cs new file mode 100644 index 0000000000..05c85d310b --- /dev/null +++ b/src/MahApps.Metro/Controls/MultiSelectionComboBox/AddingItemEventArgs.cs @@ -0,0 +1,97 @@ +// 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.Collections; +using System.Globalization; +using System.Windows; + +namespace MahApps.Metro.Controls +{ + /// + /// Provides data for the + /// + public class AddingItemEventArgs : RoutedEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The AddingItemEvent + /// The source object + /// The input string to parse + /// The parsed object + /// if the is accepted otherwise + /// The target where the should be added + /// the target to which the should be converted to + /// the string format which can be used to control the + /// the culture which can be used to control the + /// The used parser + public AddingItemEventArgs(RoutedEvent routedEvent, + object source, + string? input, + object? parsedObject, + bool accepted, + IList? targetList, + Type? targetType, + string? stringFormat, + CultureInfo culture, + IParseStringToObject? parser) + : base(routedEvent, source) + { + this.Input = input; + this.ParsedObject = parsedObject; + this.Accepted = accepted; + this.TargetList = targetList; + this.TargetType = targetType; + this.StringFormat = stringFormat; + this.Culture = culture; + this.Parser = parser; + } + + /// + /// The text input to parse + /// + public string? Input { get; } + + /// + /// Gets or sets the parsed object to add. You can override it + /// + public object? ParsedObject { get; set; } + + /// + /// Gets the string format which can be used to control the + /// + public string? StringFormat { get; } + + /// + /// Gets the culture which can be used to control the + /// + public CultureInfo Culture { get; } + + /// + /// Gets the -Instance which was used to parse the to the + /// + public IParseStringToObject? Parser { get; } + + /// + /// Gets the target to which the should be converted to + /// + public Type? TargetType { get; } + + /// + /// Gets the where the should be added + /// + public IList? TargetList { get; } + + /// + /// Gets or sets whether the is accepted and can be added + /// + public bool Accepted { get; set; } + } + + /// + /// RoutedEventHandler used for the . + /// + public delegate void AddingItemEventArgsHandler(object? sender, AddingItemEventArgs args); +} \ No newline at end of file diff --git a/src/MahApps.Metro/Controls/MultiSelectionComboBox/DefaultStringToObjectParser.cs b/src/MahApps.Metro/Controls/MultiSelectionComboBox/DefaultStringToObjectParser.cs new file mode 100644 index 0000000000..de7438a510 --- /dev/null +++ b/src/MahApps.Metro/Controls/MultiSelectionComboBox/DefaultStringToObjectParser.cs @@ -0,0 +1,73 @@ +// 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.Collections; +using System.ComponentModel; +using System.Globalization; +using System.Linq; + +namespace MahApps.Metro.Controls +{ + /// + /// This class is a helper class for the . + /// It uses the for the elements . If you need more control + /// over the conversion you should create your own class which implements + /// + public class DefaultStringToObjectParser : IParseStringToObject + { + public static readonly DefaultStringToObjectParser Instance = new(); + + /// + public bool TryCreateObjectFromString(string? input, + out object? result, + CultureInfo? culture = null, + string? stringFormat = null, + Type? targetType = null) + { + try + { + // If the input is null the result is also null + if (input is null) + { + result = null; + return true; + } + + // If we don't know the target type we cannot convert in this class. + // Either provide the target type or roll your own class implementing IParseStringToObject + if (targetType is null) + { + result = null; + return false; + } + + result = TypeDescriptor.GetConverter(targetType).ConvertFromString(default!, culture ?? CultureInfo.InvariantCulture, input); + return true; + } + catch + { + result = null; + return false; + } + } + + /// + /// Tries to get the elements for a given + /// + /// Any collection of elements + /// the elements + public Type? GetElementType(IEnumerable? list) + { + if (list is null) + { + return null; + } + + var listType = list.GetType(); + + return listType.IsGenericType ? listType.GetGenericArguments().FirstOrDefault() : listType.GetElementType(); + } + } +} \ No newline at end of file diff --git a/src/MahApps.Metro/Controls/MultiSelectionComboBox/ICompareObjectToString.cs b/src/MahApps.Metro/Controls/MultiSelectionComboBox/ICompareObjectToString.cs new file mode 100644 index 0000000000..4000344a3e --- /dev/null +++ b/src/MahApps.Metro/Controls/MultiSelectionComboBox/ICompareObjectToString.cs @@ -0,0 +1,65 @@ +// 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.Markup; + +namespace MahApps.Metro.Controls +{ + /// + /// Defines a function that is used to check if a given string represents a given object of any type. + /// + public interface ICompareObjectToString + { + /// + /// Checks if the given input string matches to the given object + /// + /// The string to compare + /// The object to compare + /// The used to check if the string matches + /// The string format to apply + /// true if the string represents the object, otherwise false. + public bool CheckIfStringMatchesObject(string? input, object? objectToCompare, StringComparison stringComparison, string? stringFormat); + } + + [MarkupExtensionReturnType(typeof(DefaultObjectToStringComparer))] + public class DefaultObjectToStringComparer : MarkupExtension, ICompareObjectToString + { + /// + public bool CheckIfStringMatchesObject(string? input, object? objectToCompare, StringComparison stringComparison, string? stringFormat) + { + if (input is null) + { + return objectToCompare is null; + } + + if (objectToCompare is null) + { + return false; + } + + string? objectText; + if (string.IsNullOrEmpty(stringFormat)) + { + objectText = objectToCompare.ToString(); + } + else if (stringFormat!.Contains("{") && stringFormat!.Contains("}")) + { + objectText = string.Format(stringFormat!, objectToCompare!); + } + else + { + objectText = string.Format($"{{0:{stringFormat}}}", objectToCompare); + } + + return input.Equals(objectText, stringComparison); + } + + /// + public override object ProvideValue(IServiceProvider serviceProvider) + { + return this; + } + } +} \ No newline at end of file diff --git a/src/MahApps.Metro/Controls/MultiSelectionComboBox/IParseStringToObject.cs b/src/MahApps.Metro/Controls/MultiSelectionComboBox/IParseStringToObject.cs new file mode 100644 index 0000000000..c21b1d4bbd --- /dev/null +++ b/src/MahApps.Metro/Controls/MultiSelectionComboBox/IParseStringToObject.cs @@ -0,0 +1,30 @@ +// 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.Globalization; + +namespace MahApps.Metro.Controls +{ + /// + /// This interfaces is used to parse a string to any object. + /// + public interface IParseStringToObject + { + /// + /// Parses the given input to an object of the given type. + /// + /// The input string to parse + /// The object if successful, otherwise null + /// The culture which should be used to parse. This parameter is optional + /// The string format to apply. This parameter is optional + /// the to which the input should be converted to. This parameter is optional + /// if converting successful, otherwise + bool TryCreateObjectFromString(string? input, + out object? result, + CultureInfo? culture = null, + string? stringFormat = null, + Type? targetType = null); + } +} \ No newline at end of file diff --git a/src/MahApps.Metro/Controls/MultiSelectionComboBox/MultiSelectionComboBox.cs b/src/MahApps.Metro/Controls/MultiSelectionComboBox/MultiSelectionComboBox.cs new file mode 100644 index 0000000000..985b31ff41 --- /dev/null +++ b/src/MahApps.Metro/Controls/MultiSelectionComboBox/MultiSelectionComboBox.cs @@ -0,0 +1,1992 @@ +// 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 ControlzEx; +using MahApps.Metro.Controls.Helper; +using MahApps.Metro.ValueBoxes; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Threading; +using JetBrains.Annotations; + +namespace MahApps.Metro.Controls +{ + [TemplatePart(Name = nameof(PART_PopupListBox), Type = typeof(ListBox))] + [TemplatePart(Name = nameof(PART_Popup), Type = typeof(Popup))] + [TemplatePart(Name = nameof(PART_SelectedItemsPresenter), Type = typeof(ListBox))] + [StyleTypedProperty(Property = nameof(SelectedItemContainerStyle), StyleTargetType = typeof(ListBoxItem))] + [StyleTypedProperty(Property = nameof(ItemContainerStyle), StyleTargetType = typeof(ListBoxItem))] + public class MultiSelectionComboBox : ComboBox + { + #region Constructors + + static MultiSelectionComboBox() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiSelectionComboBox), new FrameworkPropertyMetadata(typeof(MultiSelectionComboBox))); + TextProperty.OverrideMetadata(typeof(MultiSelectionComboBox), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.Journal, OnTextChanged)); + CommandManager.RegisterClassCommandBinding(typeof(MultiSelectionComboBox), new CommandBinding(ClearContentCommand, ExecutedClearContentCommand, CanExecuteClearContentCommand)); + CommandManager.RegisterClassCommandBinding(typeof(MultiSelectionComboBox), new CommandBinding(RemoveItemCommand, RemoveItemCommand_Executed, RemoveItemCommand_CanExecute)); + CommandManager.RegisterClassCommandBinding(typeof(MultiSelectionComboBox), new CommandBinding(SelectAllCommand, OnSelectAll, OnQueryStatusSelectAll)); + } + + public MultiSelectionComboBox() + { + var selectedItemsImpl = new ObservableCollection(); + this.SetValue(SelectedItemsPropertyKey, selectedItemsImpl); + + selectedItemsImpl.CollectionChanged += this.SelectedItemsImpl_CollectionChanged; + } + + #endregion + + //------------------------------------------------------------------- + // + // Private Members + // + //------------------------------------------------------------------- + + #region private Members + + private Popup? PART_Popup; + private ListBox? PART_PopupListBox; + private TextBox? PART_EditableTextBox; + private ListBox? PART_SelectedItemsPresenter; + + private bool isUserdefinedTextInputPending; + private bool isTextChanging; // This flag indicates if the text is changed by us, so we don't want to re-fire the TextChangedEvent. + private bool shouldDoTextReset; // Defines if the Text should be reset after selecting items from string + private bool shouldAddItems; // Defines if the MultiSelectionComboBox should add new items from text input. Don't set this to true while input is pending. We cannot know how long the user needs for typing. + private bool isSyncingSelectedItems; // true if syncing in one or the other direction already running + private DispatcherTimer? updateSelectedItemsFromTextTimer; + private static readonly RoutedUICommand SelectAllCommand + = new RoutedUICommand("Select All", + nameof(SelectAllCommand), + typeof(MultiSelectionComboBox), + new InputGestureCollection() { new KeyGesture(Key.A, ModifierKeys.Control) }); + + #endregion + + //------------------------------------------------------------------- + // + // Public Properties + // + //------------------------------------------------------------------- + + #region Public Properties + + /// Identifies the dependency property. + public static readonly DependencyProperty SelectionModeProperty + = DependencyProperty.Register(nameof(SelectionMode), + typeof(SelectionMode), + typeof(MultiSelectionComboBox), + new PropertyMetadata(SelectionMode.Single), + o => + { + var value = (SelectionMode)o; + return value == SelectionMode.Single + || value == SelectionMode.Multiple + || value == SelectionMode.Extended; + }); + + /// + /// Indicates the selection behavior for the ListBox. + /// + public SelectionMode SelectionMode + { + get => (SelectionMode)this.GetValue(SelectionModeProperty); + set => this.SetValue(SelectionModeProperty, value); + } + + /// Identifies the dependency property. + public new static readonly DependencyProperty SelectedItemProperty + = DependencyProperty.Register(nameof(SelectedItem), + typeof(object), + typeof(MultiSelectionComboBox), + new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); + + /// + /// Gets or Sets the selectedItem + /// + public new object? SelectedItem + { + get => (object?)this.GetValue(SelectedItemProperty); + set => this.SetValue(SelectedItemProperty, value); + } + + /// Identifies the dependency property. + public new static readonly DependencyProperty SelectedIndexProperty + = DependencyProperty.Register(nameof(SelectedIndex), + typeof(int), + typeof(MultiSelectionComboBox), + new FrameworkPropertyMetadata(-1, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); + + /// + /// Gets or Sets the SelectedIndex + /// + public new int SelectedIndex + { + get => (int)this.GetValue(SelectedIndexProperty); + set => this.SetValue(SelectedIndexProperty, value); + } + + /// Identifies the dependency property. + public new static readonly DependencyProperty SelectedValueProperty + = DependencyProperty.Register(nameof(SelectedValue), + typeof(object), + typeof(MultiSelectionComboBox), + new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); + + /// + /// Gets or Sets the SelectedValue + /// + public new object? SelectedValue + { + get => (object?)this.GetValue(SelectedValueProperty); + set => this.SetValue(SelectedValueProperty, value); + } + + /// Identifies the dependency property. + internal static readonly DependencyPropertyKey SelectedItemsPropertyKey + = DependencyProperty.RegisterReadOnly(nameof(SelectedItems), + typeof(IList), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// Identifies the dependency property. + public static readonly DependencyProperty SelectedItemsProperty = SelectedItemsPropertyKey.DependencyProperty; + + /// + /// The currently selected items. + /// + [Bindable(true), Category("Appearance"), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IList? SelectedItems + { + get => (IList?)this.GetValue(SelectedItemsProperty); + protected set => this.SetValue(SelectedItemsPropertyKey, value); + } + + /// Identifies the dependency property. + internal static readonly DependencyPropertyKey DisplaySelectedItemsPropertyKey + = DependencyProperty.RegisterReadOnly(nameof(DisplaySelectedItems), + typeof(IEnumerable), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// Identifies the dependency property. + public static readonly DependencyProperty DisplaySelectedItemsProperty = DisplaySelectedItemsPropertyKey.DependencyProperty; + + /// + /// Gets the in the specified order which was set via + /// + public IEnumerable? DisplaySelectedItems + { + get => (IEnumerable?)this.GetValue(DisplaySelectedItemsProperty); + protected set => this.SetValue(DisplaySelectedItemsPropertyKey, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty OrderSelectedItemsByProperty + = DependencyProperty.Register(nameof(OrderSelectedItemsBy), + typeof(SelectedItemsOrderType), + typeof(MultiSelectionComboBox), + new PropertyMetadata(SelectedItemsOrderType.SelectedOrder, OnOrderSelectedItemsByChanged)); + + /// + /// Gets or sets how the should be sorted + /// + public SelectedItemsOrderType OrderSelectedItemsBy + { + get => (SelectedItemsOrderType)this.GetValue(OrderSelectedItemsByProperty); + set => this.SetValue(OrderSelectedItemsByProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty SelectedItemContainerStyleProperty + = DependencyProperty.Register(nameof(SelectedItemContainerStyle), + typeof(Style), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or sets the for the + /// + public Style? SelectedItemContainerStyle + { + get => (Style?)this.GetValue(SelectedItemContainerStyleProperty); + set => this.SetValue(SelectedItemContainerStyleProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty SelectedItemContainerStyleSelectorProperty + = DependencyProperty.Register(nameof(SelectedItemContainerStyleSelector), + typeof(StyleSelector), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or sets the for the + /// + public StyleSelector? SelectedItemContainerStyleSelector + { + get => (StyleSelector?)this.GetValue(SelectedItemContainerStyleSelectorProperty); + set => this.SetValue(SelectedItemContainerStyleSelectorProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty SeparatorProperty + = DependencyProperty.Register(nameof(Separator), + typeof(string), + typeof(MultiSelectionComboBox), + new FrameworkPropertyMetadata(null, UpdateText)); + + /// + /// Gets or Sets the Separator which will be used if the ComboBox is editable. + /// + public string? Separator + { + get => (string?)this.GetValue(SeparatorProperty); + set => this.SetValue(SeparatorProperty, value); + } + + /// Identifies the dependency property. + internal static readonly DependencyPropertyKey HasCustomTextPropertyKey + = DependencyProperty.RegisterReadOnly(nameof(HasCustomText), + typeof(bool), + typeof(MultiSelectionComboBox), + new PropertyMetadata(BooleanBoxes.FalseBox)); + + /// Identifies the dependency property. + public static readonly DependencyProperty HasCustomTextProperty = HasCustomTextPropertyKey.DependencyProperty; + + /// + /// Indicates if the text is user defined + /// + public bool HasCustomText + { + get => (bool)this.GetValue(HasCustomTextProperty); + protected set => this.SetValue(HasCustomTextPropertyKey, BooleanBoxes.Box(value)); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty TextWrappingProperty + = TextBlock.TextWrappingProperty.AddOwner(typeof(MultiSelectionComboBox), + new FrameworkPropertyMetadata(TextWrapping.NoWrap, FrameworkPropertyMetadataOptions.AffectsMeasure)); + + /// + /// The TextWrapping property controls whether or not text wraps + /// when it reaches the flow edge of its containing block box. + /// + public TextWrapping TextWrapping + { + get => (TextWrapping)this.GetValue(TextWrappingProperty); + set => this.SetValue(TextWrappingProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty AcceptsReturnProperty + = TextBoxBase.AcceptsReturnProperty.AddOwner(typeof(MultiSelectionComboBox)); + + /// + /// The TextWrapping property controls whether or not text wraps + /// when it reaches the flow edge of its containing block box. + /// + public bool AcceptsReturn + { + get => (bool)this.GetValue(AcceptsReturnProperty); + set => this.SetValue(AcceptsReturnProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty ObjectToStringComparerProperty + = DependencyProperty.Register(nameof(ObjectToStringComparer), + typeof(ICompareObjectToString), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or Sets a function that is used to check if the entered Text is an object that should be selected. + /// + public ICompareObjectToString? ObjectToStringComparer + { + get => (ICompareObjectToString?)this.GetValue(ObjectToStringComparerProperty); + set => this.SetValue(ObjectToStringComparerProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty EditableTextStringComparisionProperty + = DependencyProperty.Register(nameof(EditableTextStringComparision), + typeof(StringComparison), + typeof(MultiSelectionComboBox), + new PropertyMetadata(StringComparison.Ordinal)); + + /// + /// Gets or Sets the that is used to check if the entered matches to the + /// + public StringComparison EditableTextStringComparision + { + get => (StringComparison)this.GetValue(EditableTextStringComparisionProperty); + set => this.SetValue(EditableTextStringComparisionProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty StringToObjectParserProperty + = DependencyProperty.Register(nameof(StringToObjectParser), + typeof(IParseStringToObject), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or Sets a parser-class that implements + /// + public IParseStringToObject? StringToObjectParser + { + get => (IParseStringToObject?)this.GetValue(StringToObjectParserProperty); + set => this.SetValue(StringToObjectParserProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty DisabledPopupOverlayContentProperty + = DependencyProperty.Register(nameof(DisabledPopupOverlayContent), + typeof(object), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or Sets the DisabledPopupOverlayContent + /// + public object? DisabledPopupOverlayContent + { + get => (object?)this.GetValue(DisabledPopupOverlayContentProperty); + set => this.SetValue(DisabledPopupOverlayContentProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty DisabledPopupOverlayContentTemplateProperty + = DependencyProperty.Register(nameof(DisabledPopupOverlayContentTemplate), + typeof(DataTemplate), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or Sets the DisabledPopupOverlayContentTemplate + /// + public DataTemplate? DisabledPopupOverlayContentTemplate + { + get => (DataTemplate?)this.GetValue(DisabledPopupOverlayContentTemplateProperty); + set => this.SetValue(DisabledPopupOverlayContentTemplateProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty SelectedItemTemplateProperty + = DependencyProperty.Register(nameof(SelectedItemTemplate), + typeof(DataTemplate), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or Sets the SelectedItemTemplate + /// + public DataTemplate? SelectedItemTemplate + { + get => (DataTemplate?)this.GetValue(SelectedItemTemplateProperty); + set => this.SetValue(SelectedItemTemplateProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty SelectedItemTemplateSelectorProperty + = DependencyProperty.Register(nameof(SelectedItemTemplateSelector), + typeof(DataTemplateSelector), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or Sets the SelectedItemTemplateSelector + /// + public DataTemplateSelector? SelectedItemTemplateSelector + { + get => (DataTemplateSelector?)this.GetValue(SelectedItemTemplateSelectorProperty); + set => this.SetValue(SelectedItemTemplateSelectorProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty SelectedItemStringFormatProperty + = DependencyProperty.Register(nameof(SelectedItemStringFormat), + typeof(string), + typeof(MultiSelectionComboBox), + new FrameworkPropertyMetadata(null, UpdateText)); + + /// + /// Gets or Sets the string format for the selected items + /// + public string? SelectedItemStringFormat + { + get => (string?)this.GetValue(SelectedItemStringFormatProperty); + set => this.SetValue(SelectedItemStringFormatProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty VerticalScrollBarVisibilityProperty + = DependencyProperty.Register(nameof(VerticalScrollBarVisibility), + typeof(ScrollBarVisibility), + typeof(MultiSelectionComboBox), + new PropertyMetadata(ScrollBarVisibility.Auto)); + + /// + /// Gets or Sets if the vertical scrollbar is visible + /// + public ScrollBarVisibility VerticalScrollBarVisibility + { + get => (ScrollBarVisibility)this.GetValue(VerticalScrollBarVisibilityProperty); + set => this.SetValue(VerticalScrollBarVisibilityProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty HorizontalScrollBarVisibilityProperty + = DependencyProperty.Register(nameof(HorizontalScrollBarVisibility), + typeof(ScrollBarVisibility), + typeof(MultiSelectionComboBox), + new PropertyMetadata(ScrollBarVisibility.Auto)); + + /// + /// Gets or Sets if the horizontal scrollbar is visible + /// + public ScrollBarVisibility HorizontalScrollBarVisibility + { + get => (ScrollBarVisibility)this.GetValue(HorizontalScrollBarVisibilityProperty); + set => this.SetValue(HorizontalScrollBarVisibilityProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty SelectedItemsPanelTemplateProperty + = DependencyProperty.Register(nameof(SelectedItemsPanelTemplate), + typeof(ItemsPanelTemplate), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or sets the for the selected items. + /// + public ItemsPanelTemplate? SelectedItemsPanelTemplate + { + get => (ItemsPanelTemplate?)this.GetValue(SelectedItemsPanelTemplateProperty); + set => this.SetValue(SelectedItemsPanelTemplateProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty SelectItemsFromTextInputDelayProperty + = DependencyProperty.Register(nameof(SelectItemsFromTextInputDelay), + typeof(int), + typeof(MultiSelectionComboBox), + new PropertyMetadata(-1)); + + /// + /// Gets or Sets the delay in milliseconds to wait before the selection is updated during text input. + /// If this value is -1 the selection will not be updated during text input. + /// Note: You also need to set to get this to work. + /// + public int SelectItemsFromTextInputDelay + { + get => (int)this.GetValue(SelectItemsFromTextInputDelayProperty); + set => this.SetValue(SelectItemsFromTextInputDelayProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty InterceptKeyboardSelectionProperty + = DependencyProperty.Register(nameof(InterceptKeyboardSelection), + typeof(bool), + typeof(MultiSelectionComboBox), + new PropertyMetadata(BooleanBoxes.TrueBox)); + + /// + /// Gets or Sets if the user can select items from the keyboard, e.g. with the ▲ ▼ Keys. + /// This property is only applied when the is + /// + public bool InterceptKeyboardSelection + { + get => (bool)this.GetValue(InterceptKeyboardSelectionProperty); + set => this.SetValue(InterceptKeyboardSelectionProperty, value); + } + + /// Identifies the dependency property. + public static readonly DependencyProperty InterceptMouseWheelSelectionProperty + = DependencyProperty.Register(nameof(InterceptMouseWheelSelection), + typeof(bool), + typeof(MultiSelectionComboBox), + new PropertyMetadata(BooleanBoxes.TrueBox)); + + /// + /// Gets or Sets if the user can select items by mouse wheel. + /// This property is only applied when the is + /// + public bool InterceptMouseWheelSelection + { + get => (bool)this.GetValue(InterceptMouseWheelSelectionProperty); + set => this.SetValue(InterceptMouseWheelSelectionProperty, value); + } + + /// + /// Resets the custom Text to the selected Items text + /// + public void ResetEditableText(bool forceUpdate = false) + { + if (this.PART_EditableTextBox is not null) + { + var oldSelectionStart = this.PART_EditableTextBox.SelectionStart; + var oldSelectionLength = this.PART_EditableTextBox.SelectionLength; + + this.SetValue(HasCustomTextPropertyKey, false); + this.UpdateEditableText(forceUpdate); + + this.PART_EditableTextBox.SelectionStart = oldSelectionStart; + this.PART_EditableTextBox.SelectionLength = oldSelectionLength; + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsDropDownHeaderVisibleProperty = + DependencyProperty.Register(nameof(IsDropDownHeaderVisible), + typeof(bool), + typeof(MultiSelectionComboBox), + new PropertyMetadata(BooleanBoxes.FalseBox)); + + /// + /// Gets or Sets if the Header in the DropDown is visible + /// + public bool IsDropDownHeaderVisible + { + get => (bool)this.GetValue(IsDropDownHeaderVisibleProperty); + set => this.SetValue(IsDropDownHeaderVisibleProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DropDownHeaderContentProperty = + DependencyProperty.Register(nameof(DropDownHeaderContent), + typeof(object), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or Sets the content of the DropDown-Header + /// + public object? DropDownHeaderContent + { + get => (object?)this.GetValue(DropDownHeaderContentProperty); + set => this.SetValue(DropDownHeaderContentProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DropDownHeaderContentTemplateProperty = + DependencyProperty.Register(nameof(DropDownHeaderContentTemplate), + typeof(DataTemplate), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or Sets the content template of the DropDown-Header + /// + public DataTemplate? DropDownHeaderContentTemplate + { + get => (DataTemplate?)this.GetValue(DropDownHeaderContentTemplateProperty); + set => this.SetValue(DropDownHeaderContentTemplateProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DropDownHeaderContentTemplateSelectorProperty = + DependencyProperty.Register(nameof(DropDownHeaderContentTemplateSelector), + typeof(DataTemplateSelector), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or Sets the content template selector of the DropDown-Header + /// + public DataTemplateSelector? DropDownHeaderContentTemplateSelector + { + get => (DataTemplateSelector?)this.GetValue(DropDownHeaderContentTemplateSelectorProperty); + set => this.SetValue(DropDownHeaderContentTemplateSelectorProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DropDownHeaderContentStringFormatProperty = + DependencyProperty.Register(nameof(DropDownHeaderContentStringFormat), + typeof(string), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or Sets the content string format of the DropDown-Header + /// + public string? DropDownHeaderContentStringFormat + { + get => (string?)this.GetValue(DropDownHeaderContentStringFormatProperty); + set => this.SetValue(DropDownHeaderContentStringFormatProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsDropDownFooterVisibleProperty = + DependencyProperty.Register(nameof(IsDropDownFooterVisible), + typeof(bool), + typeof(MultiSelectionComboBox), + new PropertyMetadata(BooleanBoxes.FalseBox)); + + /// + /// Gets or Sets if the Footer in the DropDown is visible + /// + public bool IsDropDownFooterVisible + { + get => (bool)this.GetValue(IsDropDownFooterVisibleProperty); + set => this.SetValue(IsDropDownFooterVisibleProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DropDownFooterContentProperty = + DependencyProperty.Register(nameof(DropDownFooterContent), + typeof(object), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or Sets the content of the DropDown-Footer + /// + public object? DropDownFooterContent + { + get => (object?)this.GetValue(DropDownFooterContentProperty); + set => this.SetValue(DropDownFooterContentProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DropDownFooterContentTemplateProperty = + DependencyProperty.Register(nameof(DropDownFooterContentTemplate), + typeof(DataTemplate), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or Sets the content template of the DropDown-Footer + /// + public DataTemplate? DropDownFooterContentTemplate + { + get => (DataTemplate?)this.GetValue(DropDownFooterContentTemplateProperty); + set => this.SetValue(DropDownFooterContentTemplateProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DropDownFooterContentTemplateSelectorProperty = + DependencyProperty.Register(nameof(DropDownFooterContentTemplateSelector), + typeof(DataTemplateSelector), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or Sets the content template selector of the DropDown-Footer + /// + public DataTemplateSelector? DropDownFooterContentTemplateSelector + { + get => (DataTemplateSelector?)this.GetValue(DropDownFooterContentTemplateSelectorProperty); + set => this.SetValue(DropDownFooterContentTemplateSelectorProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DropDownFooterContentStringFormatProperty = + DependencyProperty.Register(nameof(DropDownFooterContentStringFormat), + typeof(string), + typeof(MultiSelectionComboBox), + new PropertyMetadata(null)); + + /// + /// Gets or Sets the content string format of the DropDown-Footer + /// + public string? DropDownFooterContentStringFormat + { + get => (string?)this.GetValue(DropDownFooterContentStringFormatProperty); + set => this.SetValue(DropDownFooterContentStringFormatProperty, value); + } + + #endregion + + #region Methods + + /// + /// Updates the Text of the editable TextBox. + /// Sets the custom Text if any otherwise the concatenated string. + /// + private void UpdateEditableText(bool forceUpdate = false) + { + if (this.PART_EditableTextBox is null || (this.PART_EditableTextBox.IsKeyboardFocused && !forceUpdate)) + { + return; + } + + this.isTextChanging = true; + + var oldSelectionStart = this.PART_EditableTextBox.SelectionStart; + var oldSelectionLength = this.PART_EditableTextBox.SelectionLength; + var oldTextLength = this.PART_EditableTextBox.Text.Length; + + var selectedItemsText = this.GetSelectedItemsText(); + + if (!this.HasCustomText) + { + this.SetCurrentValue(TextProperty, selectedItemsText); + } + + this.UpdateHasCustomText(selectedItemsText); + + if (oldSelectionLength == oldTextLength) // We had all Text selected, so we select all again + { + this.PART_EditableTextBox.SelectionStart = 0; + this.PART_EditableTextBox.SelectionLength = this.PART_EditableTextBox.Text.Length; + } + else if (oldSelectionStart == oldTextLength) // we had the cursor at the last position, so we move the cursor to the end again + { + this.PART_EditableTextBox.SelectionStart = this.PART_EditableTextBox.Text.Length; + } + else // we restore the old selection + { + this.PART_EditableTextBox.SelectionStart = oldSelectionStart; + this.PART_EditableTextBox.SelectionLength = oldSelectionLength; + } + + this.isTextChanging = false; + } + + private void UpdateDisplaySelectedItems() + { + this.UpdateDisplaySelectedItems(this.OrderSelectedItemsBy); + } + + public string? GetSelectedItemsText() + { + switch (this.SelectionMode) + { + case SelectionMode.Single: + if (this.ReadLocalValue(DisplayMemberPathProperty) != DependencyProperty.UnsetValue + || this.ReadLocalValue(SelectedItemStringFormatProperty) != DependencyProperty.UnsetValue) + { + return BindingHelper.Eval(this.SelectedItem, this.DisplayMemberPath ?? string.Empty, this.SelectedItemStringFormat)?.ToString(); + } + else + { + return this.SelectedItem?.ToString(); + } + + case SelectionMode.Multiple: + case SelectionMode.Extended: + IEnumerable? values; + + if (this.ReadLocalValue(DisplayMemberPathProperty) != DependencyProperty.UnsetValue + || this.ReadLocalValue(SelectedItemStringFormatProperty) != DependencyProperty.UnsetValue) + { + values = this.DisplaySelectedItems?.OfType().Select(o => BindingHelper.Eval(o, this.DisplayMemberPath ?? string.Empty, this.SelectedItemStringFormat)) as IEnumerable; + } + else + { + values = this.DisplaySelectedItems as IEnumerable; + } + + return values is null ? null : string.Join(this.Separator ?? string.Empty, values); + + default: + return null; + } + } + + private void UpdateHasCustomText(string? selectedItemsText) + { + // if the parameter was null lets get the text on our own. + selectedItemsText ??= this.GetSelectedItemsText(); + + this.HasCustomText = !((string.IsNullOrEmpty(selectedItemsText) && string.IsNullOrEmpty(this.Text)) + || string.Equals(this.Text, selectedItemsText, this.EditableTextStringComparision)); + } + + private void UpdateDisplaySelectedItems(SelectedItemsOrderType selectedItemsOrderType) + { + var displaySelectedItems = selectedItemsOrderType switch + { + SelectedItemsOrderType.SelectedOrder => this.SelectedItems, + SelectedItemsOrderType.ItemsSourceOrder => (this.SelectedItems as IEnumerable)?.OrderBy(o => this.Items.IndexOf(o)), + _ => this.DisplaySelectedItems + }; + + this.SetValue(DisplaySelectedItemsPropertyKey, displaySelectedItems); + } + + private void SelectItemsFromText(int millisecondsToWait) + { + if (!this.isUserdefinedTextInputPending || this.isTextChanging) + { + return; + } + + // We want to do a text reset or add items only if we don't need to wait for more input. + this.shouldDoTextReset = millisecondsToWait == 0; + this.shouldAddItems = millisecondsToWait == 0; + + if (this.updateSelectedItemsFromTextTimer is null) + { + this.updateSelectedItemsFromTextTimer = new DispatcherTimer(DispatcherPriority.Background); + this.updateSelectedItemsFromTextTimer.Tick += this.UpdateSelectedItemsFromTextTimer_Tick; + } + + if (this.updateSelectedItemsFromTextTimer.IsEnabled) + { + this.updateSelectedItemsFromTextTimer.Stop(); + } + + if (this.ObjectToStringComparer is not null && (!string.IsNullOrEmpty(this.Separator) || this.SelectionMode == SelectionMode.Single)) + { + this.updateSelectedItemsFromTextTimer.Interval = TimeSpan.FromMilliseconds(millisecondsToWait > 0 ? millisecondsToWait : 0); + this.updateSelectedItemsFromTextTimer.Start(); + } + } + +#if NET5_0_OR_GREATER || NETCOREAPP + private void UpdateSelectedItemsFromTextTimer_Tick(object? sender, EventArgs e) +#else + private void UpdateSelectedItemsFromTextTimer_Tick(object sender, EventArgs e) +#endif + { + this.updateSelectedItemsFromTextTimer?.Stop(); + + // We clear the selection if there is no text available. + if (string.IsNullOrEmpty(this.Text)) + { + switch (this.SelectionMode) + { + case SelectionMode.Single: + this.SetCurrentValue(SelectedItemProperty, null); + break; + case SelectionMode.Multiple: + case SelectionMode.Extended: + this.SelectedItems?.Clear(); + break; + default: + throw new NotSupportedException("Unknown SelectionMode"); + } + + return; + } + + bool foundItem; + + switch (this.SelectionMode) + { + case SelectionMode.Single: + this.SetCurrentValue(SelectedItemProperty, null); + + foundItem = false; + if (this.ObjectToStringComparer is not null) + { + foreach (var item in this.Items) + { + if (this.ObjectToStringComparer.CheckIfStringMatchesObject(this.Text, item, this.EditableTextStringComparision, this.SelectedItemStringFormat)) + { + this.SetCurrentValue(SelectedItemProperty, item); + foundItem = true; + break; + } + } + } + + if (!foundItem) + { + // We try to add a new item. If we were able to do so we need to update the text as it may differ. + if (this.shouldAddItems && this.TryAddObjectFromString(this.Text, out var result)) + { + this.SetCurrentValue(SelectedItemProperty, result); + } + else + { + this.shouldDoTextReset = false; // We did not find the needed item so we should not do the text reset. + } + } + + break; + + case SelectionMode.Multiple: + case SelectionMode.Extended: + if (this.SelectedItems is null) + { + break; // Exit here if we have no SelectedItems yet + } + + var strings = !string.IsNullOrEmpty(this.Separator) ? this.Text?.Split(new[] { this.Separator }, StringSplitOptions.RemoveEmptyEntries) : null; + + int position = 0; + + if (strings is not null) + { + foreach (var stringObject in strings) + { + foundItem = false; + if (this.ObjectToStringComparer is not null) + { + foreach (var item in this.Items) + { + if (this.ObjectToStringComparer.CheckIfStringMatchesObject(stringObject, item, this.EditableTextStringComparision, this.SelectedItemStringFormat)) + { + var oldPosition = this.SelectedItems.IndexOf(item); + + if (oldPosition < 0) // if old index is <0 the item is not in list yet. let's add it. + { + this.SelectedItems.Insert(position, item); + foundItem = true; + position++; + } + else if (oldPosition > position) // if the item has a wrong position in list we need to swap it accordingly. + { + this.SelectedItems.RemoveAt(oldPosition); + this.SelectedItems.Insert(position, item); + + foundItem = true; + position++; + } + else if (oldPosition == position) + { + foundItem = true; + position++; + } + } + } + } + + if (!foundItem) + { + if (this.shouldAddItems && this.TryAddObjectFromString(stringObject, out var result)) + { + this.SelectedItems.Insert(position, result); + position++; + } + else + { + this.shouldDoTextReset = false; + } + } + } + } + + // Remove Items if needed. + while (this.SelectedItems.Count > position) + { + this.SelectedItems.RemoveAt(position); + } + + break; + + default: + throw new NotSupportedException("Unknown SelectionMode"); + } + + // First we need to check if the string matches completely to the selected items. Therefore we need to display the items in the selected order first + this.UpdateDisplaySelectedItems(SelectedItemsOrderType.SelectedOrder); + this.UpdateHasCustomText(null); + + // If the items should be ordered differently than above we need to reorder them accordingly. + if (this.OrderSelectedItemsBy != SelectedItemsOrderType.SelectedOrder) + { + this.UpdateDisplaySelectedItems(); + } + + if (this.PART_EditableTextBox is not null) + { + // We do a text reset if all items were successfully found and we don't have to wait for more input. + if (this.shouldDoTextReset) + { + var oldCaretPos = this.PART_EditableTextBox.CaretIndex; + this.ResetEditableText(); + this.PART_EditableTextBox.CaretIndex = oldCaretPos; + } + + // If we have the KeyboardFocus we need to update the text later in order to not interrupt the user. + // Therefore we connect this flag to the KeyboardFocus of the TextBox. + this.isUserdefinedTextInputPending = this.PART_EditableTextBox.IsKeyboardFocused; + } + } + + private bool TryAddObjectFromString(string? input, out object? result) + { + try + { + if (this.StringToObjectParser is null) + { + result = null; + return false; + } + + var elementType = DefaultStringToObjectParser.Instance.GetElementType(this.ItemsSource); + + var foundItem = this.StringToObjectParser.TryCreateObjectFromString(input, out result, this.Language.GetEquivalentCulture(), this.SelectedItemStringFormat, elementType); + + var addingItemEventArgs = new AddingItemEventArgs(AddingItemEvent, + this, + input, + result, + foundItem, + this.ItemsSource as IList, + elementType, + this.SelectedItemStringFormat, + this.Language.GetEquivalentCulture(), + this.StringToObjectParser); + + this.RaiseEvent(addingItemEventArgs); + + if (addingItemEventArgs.Handled) + { + addingItemEventArgs.Accepted = false; + } + + // If the adding event was not handled and the item is marked as accepted and we are allowed to modify the items list we can add the pared item + if (addingItemEventArgs.Accepted && (!addingItemEventArgs.TargetList?.IsReadOnly ?? false)) + { + addingItemEventArgs.TargetList?.Add(addingItemEventArgs.ParsedObject); + + this.RaiseEvent(new AddedItemEventArgs(AddedItemEvent, this, addingItemEventArgs.ParsedObject, addingItemEventArgs.TargetList)); + } + + result = addingItemEventArgs.ParsedObject; + return addingItemEventArgs.Accepted; + } + catch (Exception e) + { + Trace.WriteLine(e.Message); + result = null; + return false; + } + } + + #endregion + + #region Commands + + // Clear Text Command + public static RoutedUICommand ClearContentCommand { get; } = new RoutedUICommand("ClearContent", nameof(ClearContentCommand), typeof(MultiSelectionComboBox)); + + private static void ExecutedClearContentCommand(object sender, ExecutedRoutedEventArgs e) + { + if (sender is MultiSelectionComboBox multiSelectionCombo) + { + if (multiSelectionCombo.HasCustomText) + { + multiSelectionCombo.ResetEditableText(true); + } + else + { + switch (multiSelectionCombo.SelectionMode) + { + case SelectionMode.Single: + multiSelectionCombo.SetCurrentValue(SelectedItemProperty, null); + break; + case SelectionMode.Multiple: + case SelectionMode.Extended: + multiSelectionCombo.SelectedItems?.Clear(); + break; + default: + throw new NotSupportedException("Unknown SelectionMode"); + } + } + + multiSelectionCombo.ResetEditableText(true); + } + } + + private static void CanExecuteClearContentCommand(object sender, CanExecuteRoutedEventArgs e) + { + e.CanExecute = false; + if (sender is MultiSelectionComboBox multiSelectionComboBox) + { + e.CanExecute = !string.IsNullOrEmpty(multiSelectionComboBox.Text) || multiSelectionComboBox.SelectedItems?.Count > 0; + } + } + + public static RoutedUICommand RemoveItemCommand { get; } = new RoutedUICommand("Remove item", nameof(RemoveItemCommand), typeof(MultiSelectionComboBox)); + + private static void RemoveItemCommand_Executed(object sender, ExecutedRoutedEventArgs e) + { + if (sender is MultiSelectionComboBox multiSelectionCombo) + { + if (multiSelectionCombo.SelectionMode == SelectionMode.Single) + { + multiSelectionCombo.SetCurrentValue(SelectedItemProperty, null); + return; + } + + if (multiSelectionCombo.SelectedItems is not null && multiSelectionCombo.SelectedItems.Contains(e.Parameter)) + { + multiSelectionCombo.SelectedItems.Remove(e.Parameter); + } + } + } + + private static void RemoveItemCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) + { + e.CanExecute = false; + if (sender is MultiSelectionComboBox) + { + e.CanExecute = e.Parameter != null; + } + } + + #endregion + + #region Overrides + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + // Init SelectedItemsPresenter + var selectedItemsPresenterName = nameof(this.PART_SelectedItemsPresenter); + this.PART_SelectedItemsPresenter = this.GetTemplateChild(selectedItemsPresenterName) as ListBox ?? throw new MissingRequiredTemplatePartException(this, selectedItemsPresenterName); + + this.PART_SelectedItemsPresenter.MouseLeftButtonUp -= this.PART_SelectedItemsPresenter_MouseLeftButtonUp; + this.PART_SelectedItemsPresenter.MouseLeftButtonUp += this.PART_SelectedItemsPresenter_MouseLeftButtonUp; + this.PART_SelectedItemsPresenter.SelectionChanged -= this.PART_SelectedItemsPresenter_SelectionChanged; + this.PART_SelectedItemsPresenter.SelectionChanged += this.PART_SelectedItemsPresenter_SelectionChanged; + + // Init EditableTextBox + this.PART_EditableTextBox = this.GetTemplateChild(nameof(this.PART_EditableTextBox)) as TextBox ?? throw new MissingRequiredTemplatePartException(this, nameof(this.PART_EditableTextBox)); + + this.PART_EditableTextBox.LostFocus -= this.PART_EditableTextBox_LostFocus; + this.PART_EditableTextBox.LostFocus += this.PART_EditableTextBox_LostFocus; + + // Init Popup + this.PART_Popup = this.GetTemplateChild(nameof(this.PART_Popup)) as Popup ?? throw new MissingRequiredTemplatePartException(this, nameof(this.PART_Popup)); + this.PART_PopupListBox = this.GetTemplateChild(nameof(this.PART_PopupListBox)) as ListBox ?? throw new MissingRequiredTemplatePartException(this, nameof(this.PART_PopupListBox)); + + if (this.PART_PopupListBox.SelectedItems is INotifyCollectionChanged selectedItemsCollection) + { + selectedItemsCollection.CollectionChanged -= this.PART_PopupListBox_SelectedItems_CollectionChanged; + selectedItemsCollection.CollectionChanged += this.PART_PopupListBox_SelectedItems_CollectionChanged; + } + + this.SyncSelectedItems(this.SelectedItems, this.PART_PopupListBox.SelectedItems, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + + // Do update the text and selection + this.UpdateDisplaySelectedItems(); + this.UpdateEditableText(true); + } + +#if NET5_0_OR_GREATER + private void PART_PopupListBox_SelectedItems_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) +#else + private void PART_PopupListBox_SelectedItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) +#endif + { + this.SyncSelectedItems(this.PART_PopupListBox?.SelectedItems, this.SelectedItems, e); + } + + protected override void OnSelectionChanged(SelectionChangedEventArgs e) + { + base.OnSelectionChanged(e); + this.UpdateDisplaySelectedItems(); + this.UpdateEditableText(); + } + + private void MultiSelectionComboBox_Loaded(object sender, EventArgs e) + { + this.Loaded -= this.MultiSelectionComboBox_Loaded; + + if (this.PART_PopupListBox is not null) + { + // If we have the ItemsSource set, we need to exit here. + if (((this.PART_PopupListBox.Items as IList)?.IsReadOnly ?? false) || BindingOperations.IsDataBound(this.PART_PopupListBox, ItemsSourceProperty)) + { + return; + } + + this.PART_PopupListBox.Items.Clear(); + foreach (var item in this.Items) + { + this.PART_PopupListBox.Items.Add(item); + } + } + } + + protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e) + { + base.OnItemsChanged(e); + + if (!this.IsLoaded) + { + this.Loaded += this.MultiSelectionComboBox_Loaded; + return; + } + + // If we have the ItemsSource set, we need to exit here. + if (((this.PART_PopupListBox?.Items as IList)?.IsReadOnly ?? false) || BindingOperations.IsDataBound(this, ItemsSourceProperty)) + { + return; + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + if (e.NewItems is not null) + { + foreach (var item in e.NewItems) + { + this.PART_PopupListBox?.Items?.Add(item); + } + } + + break; + + case NotifyCollectionChangedAction.Remove: + if (e.OldItems is not null) + { + foreach (var item in e.OldItems) + { + this.PART_PopupListBox?.Items?.Remove(item); + } + } + + break; + + case NotifyCollectionChangedAction.Replace: + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Reset: + this.PART_PopupListBox?.Items.Clear(); + foreach (var item in this.Items) + { + this.PART_PopupListBox?.Items?.Add(item); + } + + break; + default: + throw new NotSupportedException("Unsupported NotifyCollectionChangedAction"); + } + } + + protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) + { + base.OnRenderSizeChanged(sizeInfo); + + // For now we only want to update our position if the height changed. Else we will get a flickering in SharedGridColumns + if (this.IsDropDownOpen && sizeInfo.HeightChanged) + { + this.BeginInvoke(multiSelectionComboBox => + { + if (multiSelectionComboBox.PART_Popup is not null) + { + multiSelectionComboBox.PART_Popup.HorizontalOffset++; + multiSelectionComboBox.PART_Popup.HorizontalOffset--; + } + }); + } + } + + protected override void OnDropDownOpened(EventArgs e) + { + base.OnDropDownOpened(e); + + if (this.PART_PopupListBox is not null) + { + this.PART_PopupListBox.Focus(); + + if (this.PART_PopupListBox.Items.Count == 0) + { + return; + } + } + + this.MoveFocusToDropDown(); + + this.SelectItemsFromText(0); + } + + /// + /// Sets the Keyboard focus to the dropdown + /// + private void MoveFocusToDropDown() + { + if (this.PART_PopupListBox is null || this.PART_Popup is null) + { + return; + } + + var index = this.PART_PopupListBox.SelectedIndex; + if (index < 0 && this.PART_PopupListBox.Items.Count > 0) + { + index = 0; + } + + this.BeginInvoke(() => + { + ListBoxItem? item = null; + if (index >= 0) + { + this.PART_PopupListBox.ScrollIntoView(this.PART_PopupListBox.Items[index]); + item = this.PART_PopupListBox.ItemContainerGenerator.ContainerFromIndex(index) as ListBoxItem; + } + + if (item is not null) + { + item.Focus(); + KeyboardNavigationEx.Focus(item); + this.PART_PopupListBox.ScrollIntoView(item); + } + else + { + this.PART_Popup.Focus(); + } + }, + DispatcherPriority.Send); + } + + /// + /// Return true if the item is (or is eligible to be) its own ItemUI + /// + protected override bool IsItemItsOwnContainerOverride(object item) + { + return (item is ListBoxItem); + } + + /// Create or identify the element used to display the given item. + protected override DependencyObject GetContainerForItemOverride() + { + return new ListBoxItem(); + } + + protected override void OnPreviewMouseWheel(MouseWheelEventArgs e) + { + if (this.PART_PopupListBox is null || this.PART_EditableTextBox is null) + { + return; + } + + if (this.IsEditable && !this.IsDropDownOpen && !this.InterceptKeyboardSelection) + { + if (this.HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled && ScrollViewerHelper.GetIsHorizontalScrollWheelEnabled(this)) + { + if (e.Delta > 0) + { + this.PART_EditableTextBox.LineLeft(); + } + else + { + this.PART_EditableTextBox.LineRight(); + } + } + else + { + if (e.Delta > 0) + { + this.PART_EditableTextBox.LineUp(); + } + else + { + this.PART_EditableTextBox.LineDown(); + } + } + } + else if (!this.IsEditable && !this.IsDropDownOpen && !this.InterceptMouseWheelSelection) + { + var scrollViewer = this.PART_SelectedItemsPresenter.FindChild(); + if (scrollViewer?.HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled && ScrollViewerHelper.GetIsHorizontalScrollWheelEnabled(this)) + { + if (e.Delta > 0) + { + scrollViewer?.LineLeft(); + } + else + { + scrollViewer?.LineRight(); + } + } + else + { + if (e.Delta > 0) + { + scrollViewer?.LineUp(); + } + else + { + scrollViewer?.LineDown(); + } + } + } + // ListBox eats the selection so we need to handle this event here if we want to select the next item. + else if (!this.IsDropDownOpen && this.InterceptMouseWheelSelection && this.SelectionMode == SelectionMode.Single) + { + if (e.Delta > 0 && this.PART_PopupListBox.SelectedIndex > 0) + { + this.SelectPrev(); + } + else if (e.Delta < 0 && this.PART_PopupListBox.SelectedIndex < this.PART_PopupListBox.Items.Count - 1) + { + this.SelectNext(); + } + } + + // The event is handled if the drop down is not open. + e.Handled = !this.IsDropDownOpen; + base.OnPreviewMouseWheel(e); + } + + /// + /// An event reporting a key was pressed + /// + protected override void OnPreviewKeyDown(KeyEventArgs e) + { + // Only process preview key events if they going to our editable text box + if (this.IsEditable && this.PART_EditableTextBox is not null && ReferenceEquals(e.OriginalSource, this.PART_EditableTextBox)) + { + this.KeyDownHandler(e); + } + } + + /// + /// An event reporting a key was pressed + /// + protected override void OnKeyDown(KeyEventArgs e) + { + this.KeyDownHandler(e); + } + + private void KeyDownHandler(KeyEventArgs e) + { + var handled = false; + var key = e.Key; + + // We want to handle Alt key. Get the real key if it is Key.System. + if (key == Key.System) + { + key = e.SystemKey; + } + + // In Right to Left mode we switch Right and Left keys + var isRightToLeft = (this.FlowDirection == FlowDirection.RightToLeft); + + switch (key) + { + case Key.Up: + handled = true; + if ((e.KeyboardDevice.Modifiers & ModifierKeys.Alt) == ModifierKeys.Alt) + { + this.IsDropDownOpen = !this.IsDropDownOpen; + } + else + { + // When the drop down isn't open then focus is on the ComboBox + // and we can't use KeyboardNavigation. + if (this.IsDropDownOpen) + { + this.MoveFocusToDropDown(); + } + else if (!this.IsDropDownOpen && this.InterceptKeyboardSelection && this.SelectionMode == SelectionMode.Single) + { + this.SelectPrev(); + } + } + + break; + + case Key.Down: + handled = true; + if ((e.KeyboardDevice.Modifiers & ModifierKeys.Alt) == ModifierKeys.Alt) + { + this.IsDropDownOpen = !this.IsDropDownOpen; + } + else + { + // When the drop down isn't open then focus is on the ComboBox + // and we can't use KeyboardNavigation. + if (this.IsDropDownOpen) + { + this.MoveFocusToDropDown(); + } + else if (!this.IsDropDownOpen && this.InterceptKeyboardSelection && this.SelectionMode == SelectionMode.Single) + { + this.SelectNext(); + } + } + + break; + + case Key.F4: + if ((e.KeyboardDevice.Modifiers & ModifierKeys.Alt) == 0) + { + this.IsDropDownOpen = !this.IsDropDownOpen; + handled = true; + } + + break; + + case Key.Escape: + base.OnKeyDown(e); + break; + + case Key.Enter: + if (this.IsDropDownOpen) + { + base.OnKeyDown(e); + } + + break; + + case Key.Home: + if ((e.KeyboardDevice.Modifiers & ModifierKeys.Alt) != ModifierKeys.Alt && !this.IsEditable) + { + if (!this.IsDropDownOpen && this.InterceptKeyboardSelection && this.SelectionMode == SelectionMode.Single) + { + this.SelectFirst(); + } + + handled = true; + } + + break; + + case Key.End: + if ((e.KeyboardDevice.Modifiers & ModifierKeys.Alt) != ModifierKeys.Alt && !this.IsEditable) + { + if (!this.IsDropDownOpen && this.InterceptKeyboardSelection && this.SelectionMode == SelectionMode.Single) + { + this.SelectLast(); + } + + handled = true; + } + + break; + + case Key.Right: + if ((e.KeyboardDevice.Modifiers & ModifierKeys.Alt) != ModifierKeys.Alt && !this.IsEditable) + { + if (this.IsDropDownOpen) + { + this.MoveFocusToDropDown(); + } + else + { + if (!isRightToLeft) + { + this.SelectNext(); + } + else if (!this.IsDropDownOpen && this.InterceptKeyboardSelection && this.SelectionMode == SelectionMode.Single) + { + // If it's RTL then Right should go backwards + this.SelectPrev(); + } + } + + handled = true; + } + + break; + + case Key.Left: + if ((e.KeyboardDevice.Modifiers & ModifierKeys.Alt) != ModifierKeys.Alt && !this.IsEditable) + { + if (this.IsDropDownOpen) + { + this.MoveFocusToDropDown(); + } + else if (!this.IsDropDownOpen && this.InterceptKeyboardSelection && this.SelectionMode == SelectionMode.Single) + { + if (!isRightToLeft) + { + this.SelectPrev(); + } + else + { + // If it's RTL then Left should go the other direction + this.SelectNext(); + } + } + + handled = true; + } + + break; + + case Key.PageUp: + if (this.IsDropDownOpen) + { + // At the moment this feature is not implemented for this control. + handled = true; + } + + break; + + case Key.PageDown: + if (this.IsDropDownOpen) + { + // At the moment this feature is not implemented for this control. + handled = true; + } + + break; + + case Key.Oem5: + if (Keyboard.Modifiers == ModifierKeys.Control) + { + // At the moment this feature is not implemented for this control. + handled = true; + } + + break; + + default: + handled = false; + break; + } + + if (handled) + { + e.Handled = true; + } + } + + // adopted from original ComoBox + private void SelectPrev() + { + if (!this.Items.IsEmpty) + { + // Search backwards from SelectedIndex - 1 but don't start before the beginning. + // If SelectedIndex is less than 0, there is nothing to select before this item. + if (this.SelectedIndex > 0) + { + this.SelectItemHelper(this.SelectedIndex - 1, -1, -1); + } + } + } + + // adopted from original ComoBox + private void SelectNext() + { + var count = this.Items.Count; + if (count > 0) + { + // Search forwards from SelectedIndex + 1 but don't start past the end. + // If SelectedIndex is before the last item then there is potentially + // something afterwards that we could select. + if (this.SelectedIndex < count - 1) + { + this.SelectItemHelper(this.SelectedIndex + 1, +1, count); + } + } + } + + // adopted from original ComoBox + private void SelectFirst() + { + this.SelectItemHelper(0, +1, this.Items.Count); + } + + // adopted from original ComoBox + private void SelectLast() + { + this.SelectItemHelper(this.Items.Count - 1, -1, -1); + } + + // adopted from original ComoBox + // Walk in the specified direction until we get to a selectable + // item or to the stopIndex. + // NOTE: stopIndex is not inclusive (it should be one past the end of the range) + private void SelectItemHelper(int startIndex, int increment, int stopIndex) + { + Debug.Assert((increment > 0 && startIndex <= stopIndex) || (increment < 0 && startIndex >= stopIndex), "Infinite loop detected"); + + for (var i = startIndex; i != stopIndex; i += increment) + { + // If the item is selectable and the wrapper is selectable, select it. + // Need to check both because the user could set any combination of + // IsSelectable and IsEnabled on the item and wrapper. + var item = this.Items[i]; + var container = this.ItemContainerGenerator.ContainerFromIndex(i); + if (IsSelectableHelper(item) && IsSelectableHelper(container)) + { + this.SelectedIndex = i; + this.UpdateEditableText(true); // We force the update of the text + this.isUserdefinedTextInputPending = false; + break; + } + } + } + + // adopted from original ComoBox + private static bool IsSelectableHelper(object o) + { + var d = o as DependencyObject; + // If o is not a DependencyObject, it is just a plain + // object and must be selectable and enabled. + if (d == null) + { + return true; + } + + // It's selectable if IsSelectable is true and IsEnabled is true. + return (bool)d.GetValue(IsEnabledProperty); + } + + /// + /// Select all the items + /// + public void SelectAll() + { + this.PART_PopupListBox?.SelectAll(); + } + + private static void OnSelectAll(object target, ExecutedRoutedEventArgs args) + { + if (target is MultiSelectionComboBox comboBox) + { + comboBox.SelectAll(); + } + } + + private static void OnQueryStatusSelectAll(object target, CanExecuteRoutedEventArgs args) + { + if (target is MultiSelectionComboBox comboBox) + { + args.CanExecute + = comboBox.SelectionMode == SelectionMode.Extended + && comboBox.IsDropDownOpen + && !(comboBox.PART_EditableTextBox?.IsKeyboardFocused ?? false); + } + } + + /// + /// Clears all of the selected items. + /// + [PublicAPI] + public void UnselectAll() + { + this.PART_PopupListBox?.UnselectAll(); + } + + #endregion + + #region Events + +#if NET5_0_OR_GREATER + private void SelectedItemsImpl_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) +#else + private void SelectedItemsImpl_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) +#endif + { + if (this.PART_PopupListBox is null) + { + return; + } + + this.SyncSelectedItems(sender as IList, this.PART_PopupListBox.SelectedItems, e); + } + + private void SyncSelectedItems(IList? sourceCollection, IList? targetCollection, NotifyCollectionChangedEventArgs e) + { + if (this.isSyncingSelectedItems || sourceCollection is null || targetCollection is null || !this.IsInitialized) + { + return; + } + + this.isSyncingSelectedItems = true; + + try + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + if (e.NewItems is not null) + { + foreach (var item in e.NewItems) + { + targetCollection.Add(item); + } + } + + break; + case NotifyCollectionChangedAction.Remove: + if (e.OldItems is not null) + { + foreach (var item in e.OldItems) + { + targetCollection.Remove(item); + } + } + + break; + case NotifyCollectionChangedAction.Replace: + if (e.NewItems is not null) + { + foreach (var item in e.NewItems) + { + targetCollection.Add(item); + } + } + + if (e.OldItems is not null) + { + foreach (var item in e.OldItems) + { + targetCollection.Remove(item); + } + } + + break; + case NotifyCollectionChangedAction.Move: + if (e.OldItems is not null) + { + var itemCount = e.OldItems.Count; + + // for the number of items being removed, remove the item from the Old Starting Index + // (this will cause following items to be shifted down to fill the hole). + for (var i = 0; i < itemCount; i++) + { + targetCollection.RemoveAt(e.OldStartingIndex); + } + } + + if (e.NewItems is not null) + { + var itemCount = e.NewItems.Count; + + for (var i = 0; i < itemCount; i++) + { + var insertionPoint = e.NewStartingIndex + i; + + if (insertionPoint > targetCollection.Count) + { + targetCollection.Add(e.NewItems[i]); + } + else + { + targetCollection.Insert(insertionPoint, e.NewItems[i]); + } + } + } + + break; + case NotifyCollectionChangedAction.Reset: + targetCollection.Clear(); + + foreach (var item in sourceCollection) + { + targetCollection.Add(item); + } + + break; + } + + this.UpdateDisplaySelectedItems(); + this.UpdateEditableText(); + this.UpdateHasCustomText(null); + } + finally + { + this.isSyncingSelectedItems = false; + } + } + + private void PART_EditableTextBox_LostFocus(object sender, RoutedEventArgs e) + { + this.SelectItemsFromText(0); + } + + private void PART_SelectedItemsPresenter_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + // 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)); + } + + private void PART_SelectedItemsPresenter_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + // We don't want the SelectedItems to be selectable. So anytime the selection will be changed we will reset it. + this.PART_SelectedItemsPresenter?.SetCurrentValue(SelectedItemProperty, null); + } + + private static void UpdateText(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is MultiSelectionComboBox multiSelectionComboBox) + { + multiSelectionComboBox.UpdateEditableText(); + } + } + + private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is MultiSelectionComboBox multiSelectionComboBox && !multiSelectionComboBox.isTextChanging) + { + multiSelectionComboBox.UpdateHasCustomText(null); + multiSelectionComboBox.isUserdefinedTextInputPending = true; + + // Select the items during typing if enabled + if (multiSelectionComboBox.SelectItemsFromTextInputDelay >= 0) + { + multiSelectionComboBox.SelectItemsFromText(multiSelectionComboBox.SelectItemsFromTextInputDelay); + } + } + } + + private static void OnOrderSelectedItemsByChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is MultiSelectionComboBox multiSelectionComboBox && !multiSelectionComboBox.HasCustomText) + { + multiSelectionComboBox.UpdateDisplaySelectedItems(); + multiSelectionComboBox.UpdateEditableText(); + } + } + + /// Identifies the routed event. + public static readonly RoutedEvent AddingItemEvent = EventManager.RegisterRoutedEvent( + nameof(AddingItem), RoutingStrategy.Bubble, typeof(AddingItemEventArgsHandler), typeof(MultiSelectionComboBox)); + + /// + /// Occurs before a new object is added to the Items-List + /// + public event AddingItemEventArgsHandler AddingItem + { + add => this.AddHandler(AddingItemEvent, value); + remove => this.RemoveHandler(AddingItemEvent, value); + } + + /// Identifies the routed event. + public static readonly RoutedEvent AddedItemEvent = EventManager.RegisterRoutedEvent( + nameof(AddedItem), RoutingStrategy.Bubble, typeof(AddedItemEventArgsHandler), typeof(MultiSelectionComboBox)); + + /// + /// Occurs before a new object is added to the Items-List + /// + public event AddedItemEventArgsHandler AddedItem + { + add => this.AddHandler(AddedItemEvent, value); + remove => this.RemoveHandler(AddedItemEvent, value); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/MahApps.Metro/Controls/MultiSelectionComboBox/SelectedItemsOrderType.cs b/src/MahApps.Metro/Controls/MultiSelectionComboBox/SelectedItemsOrderType.cs new file mode 100644 index 0000000000..56237d5910 --- /dev/null +++ b/src/MahApps.Metro/Controls/MultiSelectionComboBox/SelectedItemsOrderType.cs @@ -0,0 +1,22 @@ +// 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. + +namespace MahApps.Metro.Controls +{ + /// + /// Defines how the selected Items should be arranged for display + /// + public enum SelectedItemsOrderType + { + /// + /// Displays the selected items in the same order as they were selected + /// + SelectedOrder, + + /// + /// Displays the selected items in the same order as they are stored in the ItemsSource + /// + ItemsSourceOrder + } +} \ No newline at end of file diff --git a/src/MahApps.Metro/Lang/MultiSelectionComboBox.Designer.cs b/src/MahApps.Metro/Lang/MultiSelectionComboBox.Designer.cs new file mode 100644 index 0000000000..e29e53b687 --- /dev/null +++ b/src/MahApps.Metro/Lang/MultiSelectionComboBox.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace MahApps.Metro.Lang { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class MultiSelectionComboBox { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal MultiSelectionComboBox() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MahApps.Metro.Lang.MultiSelectionComboBox", typeof(MultiSelectionComboBox).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Reset Text. + /// + public static string ResetTextToSelectedItems { + get { + return ResourceManager.GetString("ResetTextToSelectedItems", resourceCulture); + } + } + } +} diff --git a/src/MahApps.Metro/Lang/MultiSelectionComboBox.de.resx b/src/MahApps.Metro/Lang/MultiSelectionComboBox.de.resx new file mode 100644 index 0000000000..a08321ef4f --- /dev/null +++ b/src/MahApps.Metro/Lang/MultiSelectionComboBox.de.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Text zurücksetzen + + \ No newline at end of file diff --git a/src/MahApps.Metro/Lang/MultiSelectionComboBox.resx b/src/MahApps.Metro/Lang/MultiSelectionComboBox.resx new file mode 100644 index 0000000000..7d0ed72fa5 --- /dev/null +++ b/src/MahApps.Metro/Lang/MultiSelectionComboBox.resx @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Reset Text + This text is shown on the popup overlay if the text is userdefined. + + \ No newline at end of file diff --git a/src/MahApps.Metro/MahApps.Metro.csproj b/src/MahApps.Metro/MahApps.Metro.csproj index aa61441c14..fcdd4f0a2d 100644 --- a/src/MahApps.Metro/MahApps.Metro.csproj +++ b/src/MahApps.Metro/MahApps.Metro.csproj @@ -40,10 +40,19 @@ True ColorNames.resx + + True + True + MultiSelectionComboBox.resx + ResXFileCodeGenerator ColorNames.Designer.cs + + PublicResXFileCodeGenerator + MultiSelectionComboBox.Designer.cs + diff --git a/src/MahApps.Metro/Styles/Controls.ListBox.xaml b/src/MahApps.Metro/Styles/Controls.ListBox.xaml index 5598f0814c..e51122af05 100644 --- a/src/MahApps.Metro/Styles/Controls.ListBox.xaml +++ b/src/MahApps.Metro/Styles/Controls.ListBox.xaml @@ -24,6 +24,8 @@ CornerRadius="{TemplateBinding mah:ControlsHelper.CornerRadius}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"> + + diff --git a/src/MahApps.Metro/Themes/Generic.xaml b/src/MahApps.Metro/Themes/Generic.xaml index a296f451b9..dcacd53e58 100644 --- a/src/MahApps.Metro/Themes/Generic.xaml +++ b/src/MahApps.Metro/Themes/Generic.xaml @@ -44,6 +44,7 @@ + @@ -73,5 +74,6 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MahApps.Metro/VisualStudioToolsManifest.xml b/src/MahApps.Metro/VisualStudioToolsManifest.xml index a1e74c98ca..236df5f31b 100644 --- a/src/MahApps.Metro/VisualStudioToolsManifest.xml +++ b/src/MahApps.Metro/VisualStudioToolsManifest.xml @@ -18,6 +18,7 @@ + diff --git a/src/Mahapps.Metro.Tests/Tests/MultiSelectionComboBoxTests.cs b/src/Mahapps.Metro.Tests/Tests/MultiSelectionComboBoxTests.cs new file mode 100644 index 0000000000..10e832fce4 --- /dev/null +++ b/src/Mahapps.Metro.Tests/Tests/MultiSelectionComboBoxTests.cs @@ -0,0 +1,38 @@ +// 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.Collections.Generic; +using MahApps.Metro.Controls; +using MahApps.Metro.Tests.TestHelpers; +using Xunit; + +namespace MahApps.Metro.Tests.Tests +{ + public class MultiSelectionComboBoxTests : AutomationTestBase + { + [Fact] + [DisplayTestMethodName] + public void ShouldGetElementTypeFromList() + { + var list = new List(); + var elementType = DefaultStringToObjectParser.Instance.GetElementType(list); + + Assert.Equal(typeof(MyTestClass), elementType); + + list.Add(new MyOtherTestClass()); + elementType = DefaultStringToObjectParser.Instance.GetElementType(list); + + Assert.Equal(typeof(MyTestClass), elementType); + } + } + + public class MyTestClass + { + public string TestField { get; set; } = "Test"; + } + + public class MyOtherTestClass : MyTestClass + { + } +} \ No newline at end of file diff --git a/src/Mahapps.Metro.Tests/Views/AutoWatermarkTestWindow.xaml b/src/Mahapps.Metro.Tests/Views/AutoWatermarkTestWindow.xaml index 592d388662..6820c41918 100644 --- a/src/Mahapps.Metro.Tests/Views/AutoWatermarkTestWindow.xaml +++ b/src/Mahapps.Metro.Tests/Views/AutoWatermarkTestWindow.xaml @@ -4,9 +4,8 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:tests="clr-namespace:MahApps.Metro.Tests" xmlns:views="clr-namespace:MahApps.Metro.Tests.Views" - d:DataContext="{d:DesignInstance tests:AutoWatermarkTestModel}" + d:DataContext="{d:DesignInstance views:AutoWatermarkTestModel}" d:DesignHeight="300" d:DesignWidth="300" mc:Ignorable="d">