Reputation: 2711
I am using an MVVM pattern and have a ComboBox
that binds to properties in the viewmodel like this:
<ComboBox ItemsSource="{Binding Path=ItemCollection}"
SelectedItem="{Binding Path=SelectedItem}">
<ComboBox.ItemTemplate>
<DataTemplate>
<!-- Custom combobox item template -->
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
This works fine and in the DataTemplate
I can specify how each item should be displayed.
I would like to add a button at the end of the combobox dropdown itempresenter. Similar to how its done in the image below from MS Word.
When the selected button "More Columns..." is pressed a dialog is shown and the user can input details. I am trying to archive the same workflow.
Upvotes: 0
Views: 892
Reputation: 843
I needed to create a custom drop down once with embedded search/filtering functionality and did it by creating a Button that would show a Pop Up when pressed. The Pop Up had a list box for the items and showed some buttons on top and below by using a Grid for layout. I've stripped it down a bit for you here:
<UserControl x:Class="CustomDropDown"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
mc:Ignorable="d" x:Name="Root"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Button x:Name="DropDownButton" Click="DropDownButton_Click" HorizontalContentAlignment="Stretch">
<DockPanel HorizontalAlignment="Stretch" LastChildFill="False" Margin="2,0">
<TextBlock DockPanel.Dock="Left" Text="{Binding ElementName=Root, Path=Header}"/>
<TextBlock DockPanel.Dock="Right" Text="▼"/>
</DockPanel>
</Button>
<Popup x:Name="DropDownPopup" Placement="Bottom" Focusable="False" StaysOpen="False"
Width="{Binding ElementName=DropDownButton, Path=ActualWidth}" MinWidth="250"
Height="Auto" AllowsTransparency="True">
<Border Padding="5" Background="White" Margin="0,0,8,8"
BorderBrush="Silver" CornerRadius="0,0,5,5" BorderThickness="1">
<Border.Effect>
<DropShadowEffect BlurRadius="5" Opacity="0.5"/>
</Border.Effect>
<Grid HorizontalAlignment="Stretch" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" MaxHeight="150" x:Name="DropDownList" SelectionMode="Extended"
ItemsSource="{Binding ElementName=Root, Path=ItemsSource}"
DisplayMemberPath="{Binding ElementName=Root, Path=DisplayMemberPath}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<!-- Item Style -->
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
<Grid>
<!-- More Columns Button Here -->
</Grid>
</Grid>
</Border>
</Popup>
</Grid>
</UserControl>
Because I made it a UserControl, I had some Dependency Properties for things like the Header, DisplayMemberPath, etc that I wanted to be able to define bindings for when the control was used in XAML. SelectedItems had its own dependency property and an event was registered in the code behind to keep everything in sync.
public IRangeCollection SelectedItems
{
get { return (IRangeCollection)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
// Using a DependencyProperty as the backing store for SelectedItems.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems",
typeof(IRangeCollection), typeof(CustomDropDown),
new PropertyMetadata(new PropertyChangedCallback(SelectedItemsPropertyChanged)));
private static void SelectedItemsPropertyChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
CustomDropDown dropDown = (CustomDropDown)sender;
dropDown.UpdateDropDownFromSelectedItems();
dropDown.UpdateSelectedItemsCollectionChangedHandler(e.OldValue, e.NewValue);
}
private void UpdateSelectedItemsCollectionChangedHandler(object oldValue, object newValue)
{
if (oldValue != null && oldValue is INotifyCollectionChanged)
{
((INotifyCollectionChanged)oldValue).CollectionChanged -= SelectedItems_CollectionChanged;
}
if (newValue != null && newValue is INotifyCollectionChanged)
{
((INotifyCollectionChanged)newValue).CollectionChanged += SelectedItems_CollectionChanged;
}
}
Upvotes: 0
Reputation: 6547
I don't think ComboBox
is the right choice here, as the natural behavior of a ComboBox
would be to select the clicked item while you want to open a dialog
Here is a complete code example of how to achieve something like this: 1. Create a DropDownButton behavior as following
public class DropDownButtonBehavior : Behavior<Button>
{
private long attachedCount;
private bool isContextMenuOpen;
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.AddHandler(Button.ClickEvent, new RoutedEventHandler(AssociatedObject_Click), true);
}
void AssociatedObject_Click(object sender, System.Windows.RoutedEventArgs e)
{
Button source = sender as Button;
if (source != null && source.ContextMenu != null)
{
// Only open the ContextMenu when it is not already open. If it is already open,
// when the button is pressed the ContextMenu will lose focus and automatically close.
if (!isContextMenuOpen)
{
source.ContextMenu.AddHandler(ContextMenu.ClosedEvent, new RoutedEventHandler(ContextMenu_Closed), true);
Interlocked.Increment(ref attachedCount);
// If there is a drop-down assigned to this button, then position and display it
source.ContextMenu.PlacementTarget = source;
source.ContextMenu.Placement = PlacementMode.Bottom;
source.ContextMenu.IsOpen = true;
isContextMenuOpen = true;
}
}
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.RemoveHandler(Button.ClickEvent, new RoutedEventHandler(AssociatedObject_Click));
}
void ContextMenu_Closed(object sender, RoutedEventArgs e)
{
isContextMenuOpen = false;
var contextMenu = sender as ContextMenu;
if (contextMenu != null)
{
contextMenu.RemoveHandler(ContextMenu.ClosedEvent, new RoutedEventHandler(ContextMenu_Closed));
Interlocked.Decrement(ref attachedCount);
}
}
}
Create a template for the button
<Button>
<i:Interaction.Behaviors>
<local:DropDownButtonBehavior/>
</i:Interaction.Behaviors>
<Button.Content>
<StackPanel Orientation="Horizontal">
<ContentControl Content="{Binding SelectedItem}"/>
<Separator Margin="2,0">
<Separator.LayoutTransform>
<TransformGroup>
<TransformGroup.Children>
<TransformCollection>
<RotateTransform Angle="90"/>
</TransformCollection>
</TransformGroup.Children>
</TransformGroup>
</Separator.LayoutTransform>
</Separator>
<Path Margin="2" VerticalAlignment="Center" Width="6" Fill="#FF527DB5" Stretch="Uniform" HorizontalAlignment="Right" Data="F1 M 301.14,-189.041L 311.57,-189.041L 306.355,-182.942L 301.14,-189.041 Z "/>
</StackPanel>
</Button.Content>
<Button.ContextMenu>
<ContextMenu>
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type Button}, Mode=FindAncestor}, Path=DataContext.SelectionChangedCommand}" />
<Setter Property="CommandParameter" Value="{Binding}"></Setter>
</Style>
</ContextMenu.ItemContainerStyle>
<ContextMenu.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding Source={StaticResource MyData}}" />
<MenuItem Header="More"
Command="{Binding MoreButtonCommand}"/>
</CompositeCollection>
</ContextMenu.ItemsSource>
</ContextMenu>
</Button.ContextMenu>
</Button>
The view model
public class MainWindowViewModel : BindableBase
{
private MyData _selectedItem;
public MainWindowViewModel()
{
Collection = new ObservableCollection<MyData>
{
new MyData {Data = "aaa"},
new MyData {Data = "bbb"},
};
SelectedItem = Collection.First();
// This is important. It changes the selected items upon menu item click
SelectionChangedCommand = new DelegateCommand<MyData>(data => SelectedItem = data);
MoreButtonCommand = new DelegateCommand(() => {} /* Launch dialog ... */);
}
public ObservableCollection<MyData> Collection { get; set; }
public MyData SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
OnPropertyChanged(() => SelectedItem);
}
}
public DelegateCommand<MyData> SelectionChangedCommand { get; set; }
public DelegateCommand MoreButtonCommand { get; set; }
}
Let me explain a bit of what is going on here.
Each click on the button opens up a drop down menu similar to ComboBox
behavior. The items displayed in this list are MenuItem
s generated both from binded items and static ones. The ones created from the ItemSource
launches a selectionChanged command to the ViewModel causing the selected item to change, while the static ones can launch any command you specify to it.
Hope this helps
Upvotes: 1