Skip to content

Commit a2da375

Browse files
committed
Added own MultiSelectComboBox, stepped back to MahApps.Metro v2.4.9 to make AssemblyInfoHelper work again
1 parent 0bbadc6 commit a2da375

File tree

5 files changed

+363
-6
lines changed

5 files changed

+363
-6
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<UserControl x:Class="Vereinsmeisterschaften.Controls.MultiSelectComboBox"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:converters="clr-namespace:Vereinsmeisterschaften.Converters"
5+
x:Name="root">
6+
<UserControl.Resources>
7+
<converters:SelectedItemsContainsConverter x:Key="SelectedItemsContainsConverter" />
8+
</UserControl.Resources>
9+
10+
<Grid>
11+
<Border x:Name="PART_ComboBoxContent" BorderThickness="1" BorderBrush="{DynamicResource MahApps.Brushes.ComboBox.Border.Focus}" CornerRadius="3"
12+
Background="{DynamicResource MahApps.Brushes.ComboBox.Background}" Padding="2"
13+
MinHeight="24" VerticalAlignment="Center"
14+
PreviewMouseLeftButtonUp="OnTogglePopup" MouseEnter="PART_ComboBoxContent_MouseEnter" MouseLeave="PART_ComboBoxContent_MouseLeave">
15+
<DockPanel LastChildFill="True">
16+
<Path DockPanel.Dock="Right" Margin="5,0" Width="12" Height="8" Fill="{DynamicResource MahApps.Brushes.Text}" VerticalAlignment="Center">
17+
<Path.Style>
18+
<Style TargetType="{x:Type Path}">
19+
<!-- Arrow Down -->
20+
<Setter Property="Data" Value="M 0 0 L 12 0 L 6 8 Z"/>
21+
<Style.Triggers>
22+
<DataTrigger Binding="{Binding IsPopupOpen, ElementName=root}" Value="True">
23+
<!-- Arrow Up -->
24+
<Setter Property="Data" Value="M 6 0 L 12 8 L 0 8 Z"/>
25+
</DataTrigger>
26+
</Style.Triggers>
27+
</Style>
28+
</Path.Style>
29+
</Path>
30+
31+
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
32+
<ItemsControl ItemsSource="{Binding SelectedItems, ElementName=root}">
33+
<ItemsControl.ItemsPanel>
34+
<ItemsPanelTemplate>
35+
<WrapPanel Orientation="Horizontal" />
36+
</ItemsPanelTemplate>
37+
</ItemsControl.ItemsPanel>
38+
<ItemsControl.ItemTemplate>
39+
<DataTemplate>
40+
<Border Background="{DynamicResource MahApps.Brushes.Gray2}" CornerRadius="10" Margin="2" Padding="4,0">
41+
<StackPanel Orientation="Horizontal">
42+
<ContentPresenter Content="{Binding}" ContentTemplate="{Binding ElementName=root, Path=ItemTemplate}" TextBlock.Foreground="{DynamicResource MahApps.Brushes.IdealForeground}"/>
43+
<Button Content="" Style="{DynamicResource MahApps.Styles.Button.Circle}" Background="{StaticResource BrushError}" Foreground="White"
44+
Width="20" Height="20" Margin="4,0,0,0"
45+
Command="{Binding ElementName=root, Path=RemoveItemCommand}" CommandParameter="{Binding}" />
46+
</StackPanel>
47+
</Border>
48+
</DataTemplate>
49+
</ItemsControl.ItemTemplate>
50+
</ItemsControl>
51+
</ScrollViewer>
52+
</DockPanel>
53+
</Border>
54+
55+
<!-- Popup Dropdown -->
56+
<Popup x:Name="PART_Popup" PlacementTarget="{Binding ElementName=root}" Placement="Bottom"
57+
Width="{Binding ActualWidth, ElementName=root}"
58+
IsOpen="{Binding IsPopupOpen, ElementName=root}" StaysOpen="False">
59+
<Border x:Name="PART_popupContent"
60+
Background="{DynamicResource MahApps.Brushes.Window.Background}" Padding="4"
61+
BorderBrush="{DynamicResource MahApps.Brushes.ComboBox.Border}" BorderThickness="1" CornerRadius="3" >
62+
<ScrollViewer MaxHeight="200">
63+
<ItemsControl ItemsSource="{Binding ItemsSource, ElementName=root}">
64+
<ItemsControl.ItemTemplate>
65+
<DataTemplate>
66+
<CheckBox x:Name="PART_PopupCheckbox" Checked="PART_PopupCheckbox_Checked" Unchecked="PART_PopupCheckbox_Unchecked"
67+
Foreground="{DynamicResource MahApps.Brushes.ComboBox.PopupForeground}">
68+
<CheckBox.IsChecked>
69+
<MultiBinding Mode="OneWay" Converter="{StaticResource SelectedItemsContainsConverter}">
70+
<Binding ElementName="root" Path="SelectedItems"/>
71+
<Binding Path="." />
72+
</MultiBinding>
73+
</CheckBox.IsChecked>
74+
<CheckBox.Content>
75+
<ContentPresenter Content="{Binding}" ContentTemplate="{Binding ElementName=root, Path=ItemTemplate}" />
76+
</CheckBox.Content>
77+
</CheckBox>
78+
</DataTemplate>
79+
</ItemsControl.ItemTemplate>
80+
</ItemsControl>
81+
</ScrollViewer>
82+
</Border>
83+
</Popup>
84+
</Grid>
85+
</UserControl>
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
using System.Collections;
2+
using System.Collections.ObjectModel;
3+
using System.Windows;
4+
using System.Windows.Controls;
5+
using System.Windows.Data;
6+
using System.Windows.Input;
7+
using System.Windows.Media;
8+
using CommunityToolkit.Mvvm.Input;
9+
10+
namespace Vereinsmeisterschaften.Controls
11+
{
12+
/// <summary>
13+
/// Control for a ComboBox that allows multiple selection via CheckBoxes.
14+
/// </summary>
15+
public partial class MultiSelectComboBox : UserControl
16+
{
17+
#region Constructor
18+
19+
public MultiSelectComboBox()
20+
{
21+
InitializeComponent();
22+
}
23+
24+
#endregion
25+
26+
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
27+
28+
#region Dependency Properties
29+
30+
/// <summary>
31+
/// List with all available items
32+
/// </summary>
33+
public IEnumerable ItemsSource
34+
{
35+
get => (IEnumerable)GetValue(ItemsSourceProperty);
36+
set => SetValue(ItemsSourceProperty, value);
37+
}
38+
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(nameof(ItemsSource), typeof(IEnumerable), typeof(MultiSelectComboBox), new PropertyMetadata(null));
39+
40+
/// <summary>
41+
/// List with all selected items
42+
/// </summary>
43+
public IList SelectedItems
44+
{
45+
get => (IList)GetValue(SelectedItemsProperty);
46+
set => SetValue(SelectedItemsProperty, value);
47+
}
48+
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(nameof(SelectedItems), typeof(IList), typeof(MultiSelectComboBox), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
49+
50+
/// <summary>
51+
/// Template used to display the Checkbox contents and the selected items
52+
/// </summary>
53+
public DataTemplate ItemTemplate
54+
{
55+
get => (DataTemplate)GetValue(ItemTemplateProperty);
56+
set => SetValue(ItemTemplateProperty, value);
57+
}
58+
public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register(nameof(ItemTemplate), typeof(DataTemplate), typeof(MultiSelectComboBox), new PropertyMetadata(null));
59+
60+
/// <summary>
61+
/// True, when the popup with the selection options is open
62+
/// </summary>
63+
public bool IsPopupOpen
64+
{
65+
get { return (bool)GetValue(IsPopupOpenProperty); }
66+
set { SetValue(IsPopupOpenProperty, value); }
67+
}
68+
public static readonly DependencyProperty IsPopupOpenProperty = DependencyProperty.Register(nameof(IsPopupOpen), typeof(bool), typeof(MultiSelectComboBox), new PropertyMetadata(false, OnIsPopupOpenChanged));
69+
70+
private static void OnIsPopupOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
71+
{
72+
MultiSelectComboBox control = d as MultiSelectComboBox;
73+
if(control != null && control.IsPopupOpen)
74+
{
75+
control.refreshPopupCheckBoxes();
76+
}
77+
}
78+
79+
#endregion
80+
81+
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
82+
83+
#region Popup Toggle Handling
84+
85+
/// <summary>
86+
/// Toggle the popup open/closed when clicking on the control, but not when clicking the remove button of an item.
87+
/// </summary>
88+
/// <param name="sender"></param>
89+
/// <param name="e"></param>
90+
private void OnTogglePopup(object sender, System.Windows.Input.MouseButtonEventArgs e)
91+
{
92+
if (SelectedItems == null)
93+
{
94+
SelectedItems = new ObservableCollection<object>();
95+
}
96+
97+
// Look for a Button in the visual tree of the original source (remove button of an item)
98+
// When the remove button was clicked → make sure the popup is closed
99+
Button removeButton = findParentOfType<Button>(e.OriginalSource as DependencyObject);
100+
if (removeButton != null)
101+
{
102+
IsPopupOpen = false;
103+
return;
104+
}
105+
106+
IsPopupOpen = !IsPopupOpen;
107+
}
108+
109+
// https://stopbyte.com/t/how-can-i-show-popup-on-button-click-and-hide-it-on-second-click-or-user-clicks-outside-staysopen-false/80/5
110+
private void PART_ComboBoxContent_MouseEnter(object sender, MouseEventArgs e)
111+
{
112+
PART_Popup.StaysOpen = true;
113+
}
114+
115+
// https://stopbyte.com/t/how-can-i-show-popup-on-button-click-and-hide-it-on-second-click-or-user-clicks-outside-staysopen-false/80/5
116+
private void PART_ComboBoxContent_MouseLeave(object sender, MouseEventArgs e)
117+
{
118+
PART_Popup.StaysOpen = false;
119+
}
120+
121+
/// <summary>
122+
/// Refresh the state of all CheckBoxes in the popup.
123+
/// </summary>
124+
private void refreshPopupCheckBoxes()
125+
{
126+
if (PART_Popup?.Child is DependencyObject popupChild)
127+
{
128+
foreach (var cb in findVisualChildren<CheckBox>(popupChild))
129+
{
130+
MultiBindingExpression multi = BindingOperations.GetMultiBindingExpression(cb, CheckBox.IsCheckedProperty);
131+
multi?.UpdateTarget();
132+
}
133+
}
134+
}
135+
136+
#endregion
137+
138+
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
139+
140+
#region CheckBox Handling
141+
142+
/// <summary>
143+
/// Checkbox checked event handler to add the item to the SelectedItems collection.
144+
/// </summary>
145+
/// <param name="sender">Sending <see cref="CheckBox"/></param>
146+
/// <param name="e"><see cref="RoutedEventArgs"/></param>
147+
private void PART_PopupCheckbox_Checked(object sender, RoutedEventArgs e)
148+
{
149+
if (sender is CheckBox cb && cb.DataContext != null && SelectedItems != null)
150+
{
151+
if (!SelectedItems.Contains(cb.DataContext))
152+
{
153+
SelectedItems.Add(cb.DataContext);
154+
}
155+
}
156+
}
157+
158+
/// <summary>
159+
/// Checkbox unchecked event handler to remove the item from the SelectedItems collection.
160+
/// </summary>
161+
/// <param name="sender">Sending <see cref="CheckBox"/></param>
162+
/// <param name="e"><see cref="RoutedEventArgs"/></param>
163+
private void PART_PopupCheckbox_Unchecked(object sender, RoutedEventArgs e)
164+
{
165+
if (sender is CheckBox cb && cb.DataContext != null && SelectedItems != null)
166+
{
167+
if (SelectedItems.Contains(cb.DataContext))
168+
{
169+
SelectedItems.Remove(cb.DataContext);
170+
}
171+
}
172+
}
173+
174+
#endregion
175+
176+
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
177+
178+
#region Remove Item Command
179+
180+
/// <summary>
181+
/// Command to remove an item from the SelectedItems collection.
182+
/// </summary>
183+
[ICommand]
184+
private void RemoveItem(object item)
185+
{
186+
if (item != null && SelectedItems != null && SelectedItems.Contains(item))
187+
{
188+
SelectedItems.Remove(item);
189+
}
190+
}
191+
192+
#endregion
193+
194+
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
195+
196+
#region Helper Methods
197+
198+
/// <summary>
199+
/// Find the next parent with the requested type from the child
200+
/// </summary>
201+
/// <typeparam name="T">Requested parent type</typeparam>
202+
/// <param name="child">Child to start the search from</param>
203+
/// <returns>Next parent with requested type of <see langword="null"/> if not found</returns>
204+
private T findParentOfType<T>(DependencyObject child) where T : DependencyObject
205+
{
206+
DependencyObject current = child;
207+
while (current != null)
208+
{
209+
if (current is T t)
210+
{
211+
return t;
212+
}
213+
current = VisualTreeHelper.GetParent(current);
214+
}
215+
return null;
216+
}
217+
218+
/// <summary>
219+
/// Find all visual children of a given type.
220+
/// </summary>
221+
/// <typeparam name="T">Children type</typeparam>
222+
/// <param name="depObj">Parent object to start the search from</param>
223+
/// <returns>List of children with the requested type</returns>
224+
private static IEnumerable<T> findVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
225+
{
226+
if (depObj != null)
227+
{
228+
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
229+
{
230+
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
231+
if (child != null && child is T t)
232+
{
233+
yield return t;
234+
}
235+
236+
foreach (T childOfChild in findVisualChildren<T>(child))
237+
{
238+
yield return childOfChild;
239+
}
240+
}
241+
}
242+
}
243+
244+
#endregion
245+
}
246+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Collections;
2+
using System.Globalization;
3+
using System.Windows.Data;
4+
5+
namespace Vereinsmeisterschaften.Converters
6+
{
7+
public class SelectedItemsContainsConverter : IMultiValueConverter
8+
{
9+
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
10+
{
11+
if (values.Length >= 2 &&
12+
values[0] is IList list &&
13+
values[1] != null)
14+
{
15+
return list.Contains(values[1]);
16+
}
17+
18+
return false;
19+
}
20+
21+
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
22+
{
23+
throw new NotImplementedException();
24+
}
25+
}
26+
}

Vereinsmeisterschaften/Vereinsmeisterschaften.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
4040
</PackageReference>
4141
<PackageReference Include="gong-wpf-dragdrop" Version="4.0.0" />
42-
<PackageReference Include="MahApps.Metro" Version="3.0.0-rc0529" />
42+
<PackageReference Include="MahApps.Metro" Version="2.4.9" />
4343
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
4444
<PackageReference Include="System.Reactive" Version="6.0.1" />
4545
</ItemGroup>

Vereinsmeisterschaften/Views/PeoplePage.xaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
xmlns:Dialog="clr-namespace:MahApps.Metro.Controls.Dialogs;assembly=MahApps.Metro"
1313
xmlns:coreModels="clr-namespace:Vereinsmeisterschaften.Core.Models;assembly=Vereinsmeisterschaften.Core"
1414
xmlns:viewModels="clr-namespace:Vereinsmeisterschaften.ViewModels"
15+
xmlns:userControls="clr-namespace:Vereinsmeisterschaften.Controls"
1516
Dialog:DialogParticipation.Register="{Binding}"
1617
mc:Ignorable="d"
1718
d:DesignHeight="450" d:DesignWidth="1400">
@@ -596,11 +597,10 @@
596597
<DataTemplate>
597598
<DockPanel Margin="5" LastChildFill="False">
598599
<TextBlock DockPanel.Dock="Left" Text="{Binding GroupId, StringFormat=#{0}}" VerticalAlignment="Center" Margin="0,0,20,0"/>
599-
<controls:MultiSelectionComboBox DockPanel.Dock="Left"
600-
ItemsSource="{Binding AvailableFriends}"
601-
controls:MultiSelectorHelper.SelectedItems="{Binding Friends, Mode=TwoWay}"
602-
ItemTemplate="{StaticResource DataTemplatePersonShort}"
603-
SelectedItemTemplate="{StaticResource DataTemplatePersonShort}"/>
600+
<userControls:MultiSelectComboBox MinWidth="300"
601+
ItemsSource="{Binding AvailableFriends}"
602+
SelectedItems="{Binding Friends, Mode=TwoWay}"
603+
ItemTemplate="{StaticResource DataTemplatePersonShort}" />
604604
<Button DockPanel.Dock="Right" Background="{StaticResource BrushError}"
605605
Width="30" Height="30"
606606
Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type Page}}, Path=DataContext.RemoveFriendGroupCommand}" CommandParameter="{Binding}">

0 commit comments

Comments
 (0)