zORg Alex
zORg Alex

Reputation: 381

(WPF) Editable Listbox with Add and Remove buttons

May be I'm missing something, but I think there should be a nice way to add items to ListBoxes whose contents are bound to a data source, ether than having separate button near that ListBox to trigger add_new_item() from the code.

I guess I can figure out how to add a delete button near currently selected button through style. I'm not yet sure which parameter will trigger this. But I'm more concerned with adding items like shown in the image My Awesome Custom Listbox

I'm not yet advanced into styling. What I do is, I take somebody's style and take it apart, then create what I actually need. I don't even know where to look at to see all styling techniques like looking at a class reference.

I had an idea of adding a button hanging in a corner of a ListBox or ComboBox, but how should I declare a new Event for it so I could assign unique functions to then other than one for everything.

I'm not asking for a complete solution, just a hint of what to do. I'll post an answer when I'll figure it out.

And after a year I've figured these things out.

Upvotes: 0

Views: 1255

Answers (3)

zORg Alex
zORg Alex

Reputation: 381

With time I figured these things (UVMCommand is my ICommand interface implementation, it is easier for me to use it this way):

public class Game : Notifiable {
    public Game() {
        Players = new ObservableCollection<Player>();
        AddNewPlayer = new UVMCommand("AddNewPlayer", p => {
            var pl = new Player() { Text = "New Player", IsSelected = true, IsEdited = true };
            pl.RemoveThis = new UVMCommand("RemoveThis", pp => Players.Remove(pl));
            Players.Add(pl);
        });
    }

    private ObservableCollection<Player> _players;
    public ObservableCollection<Player> Players { get { return _players; } set { _players = value; OnPropertyChanged("Players"); } }

    private UVMCommand _addNewPlayer;
    public UVMCommand AddNewPlayer { get { return _addNewPlayer; } set { _addNewPlayer = value; OnPropertyChanged("AddNewPlayer"); } }
}

public class Player : Notifiable  {
    public Player() {}

    private UVMCommand _removeThis;
    public UVMCommand RemoveThis { get { return _removeThis; } set { _removeThis = value; OnPropertyChanged("RemoveThis"); } }
}

public class Notifiable : INotifyPropertyChanged {
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName) {
        // take a copy to prevent thread issues
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

And then making a user control in code-behind declare:

public partial class EditableListBox : System.Windows.Controls.ListBox, INotifyPropertyChanged {
    public EditableListBox() {
        InitializeComponent();
        //var s = FindResource("EditableListBoxStyle") as Style;
        //Style = s;
    }

    [Category("Common")]
    public UVMCommand AddItem {
        get { return (UVMCommand)GetValue(AddItemProperty); }
        set {
            SetValue(AddItemProperty, value);
        }
    }

    // Using a DependencyProperty as the backing store for AddItem.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty AddItemProperty =
        DependencyProperty.Register("AddItem", typeof(UVMCommand), typeof(EditableListBox),
            new PropertyMetadata(
                new UVMCommand("Default Command", p => { Debug.WriteLine("EditableListBox.AddITem not defined"); })));

    [Category("Layout")]
    public Orientation Orientation {
        get { return (Orientation)GetValue(OrientationProperty); }
        set { SetValue(OrientationProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Orientation.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty OrientationProperty =
        DependencyProperty.Register("Orientation", typeof(Orientation), typeof(EditableListBox), new PropertyMetadata(Orientation.Vertical));

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName) {
        // take a copy to prevent thread issues
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

And in XAML I define a Style with an add button in a template at the end of a list and a delete button in an item template:

<ListBox x:Name="listBox" x:Class="MTCQuest.CustomControls.EditableListBox"
     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:local="clr-namespace:MTCQuest"
     xmlns:zq="clr-namespace:MTCQuest.ViewModel.zQuest;assembly=MTCQuest.ViewModel"
     xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
     xmlns:ccon="clr-namespace:MTCQuest.CustomControls"
     mc:Ignorable="d"
     d:DesignHeight="300" d:DesignWidth="300"
     Style="{DynamicResource EditableListBoxStyle}"
     ItemContainerStyle="{DynamicResource EditableListBoxItemStyle}" d:DataContext="{DynamicResource TestTheme}" ItemsSource="{Binding Questions}">
<ListBox.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="../Resources/StyleRes.xaml"/>
            <ResourceDictionary Source="../Resources/QuestSpacificControlStyles.xaml"/>
        </ResourceDictionary.MergedDictionaries>
        <ccon:BindingExists x:Key="BindingExists"/>
        <ccon:ColorToSolidColorBrushConverter x:Key="ColorToSolidColorBrushConverter"/>
        <zq:Theme x:Key="TestTheme">
            <zq:Theme.Questions>
                <zq:Question IsEdited="True" IsSelected="True" Text="Some Question" Color="#FF2E00FF"/>
                <zq:Question Text="Another Question"/>
            </zq:Theme.Questions>
        </zq:Theme>
        <Style x:Key="EditableListBoxStyle" TargetType="{x:Type ListBox}">
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="BorderBrush" Value="{StaticResource Pallete.Divider}"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="Foreground" Value="{StaticResource Pallete.PrimaryText}"/>
            <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
            <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
            <Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
            <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
            <Setter Property="Stylus.IsFlicksEnabled" Value="True"/>
            <Setter Property="VerticalContentAlignment" Value="Stretch"/>
            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
            <Setter Property="ItemsPanel" Value="{DynamicResource OrientedItemsPanelTemplate}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBox}">
                        <ScrollViewer Background="{TemplateBinding Background}"  SnapsToDevicePixels="true" Focusable="false" Padding="{TemplateBinding Padding}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
                            <StackPanel Orientation="{Binding Orientation, ElementName=listBox}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}">
                                <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                                <ccon:zButton x:Name="AddButton" Command="{Binding AddItem, RelativeSource={RelativeSource TemplatedParent}}"
                                    HorizontalContentAlignment="Left"
                                    Background="Transparent" Foreground="{DynamicResource Pallete.PrimaryText}"
                                    BorderThickness="0">
                                    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
                                        <Rectangle Width="16" Height="16" Margin="5,0"
                                            Fill="{DynamicResource Pallete.Accent}"
                                            OpacityMask="{DynamicResource Icon_PlusSign}"/>
                                        <TextBlock Text="Add" Foreground="{StaticResource Pallete.PrimaryText}"/>
                                    </StackPanel>
                                </ccon:zButton>
                            </StackPanel>
                        </ScrollViewer>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter TargetName="AddButton" Property="Visibility" Value="Visible"/>
                            </Trigger>
                            <DataTrigger Binding="{Binding}" Value="{x:Null}">
                                <Setter Property="IsEnabled" Value="False"/>
                            </DataTrigger>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Effect">
                                    <Setter.Value>
                                        <ccon:DesaturateEffect DesaturationFactor=".25"/>
                                    </Setter.Value>
                                </Setter>
                            </Trigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="IsGrouping" Value="true"/>
                                    <Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
                            </MultiTrigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Style x:Key="EditableListBoxItemStyle" TargetType="{x:Type ListBoxItem}">
            <Setter Property="SnapsToDevicePixels" Value="True"/>
            <Setter Property="Padding" Value="0"/>
            <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="BorderBrush" Value="Transparent"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="Margin" Value="5,0"/>
            <Setter Property="IsSelected" Value="{Binding IsSelected}"/>
            <Setter Property="FocusVisualStyle">
                <Setter.Value>
                    <Style>
                        <Setter Property="Control.Template">
                            <Setter.Value>
                                <ControlTemplate>
                                    <Rectangle Margin="2" SnapsToDevicePixels="True" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </Setter.Value>
            </Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBoxItem}">
                        <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Background="{TemplateBinding Background}"
                            Padding="{TemplateBinding Padding}"
                            SnapsToDevicePixels="True">
                            <Grid Height="33" >
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="*"/>
                                    <ColumnDefinition Width="Auto"/>
                                    <ColumnDefinition Width="Auto"/>
                                </Grid.ColumnDefinitions>
                                <TextBox x:Name="TB" Text="{Binding Text}"
                                    VerticalContentAlignment="Center"
                                    HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                    VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                    Padding="5, 0" BorderThickness="0" GotFocus="TB_GotFocus"
                                    Visibility="Collapsed"/>
                                <Label x:Name="Lb" Content="{Binding Text}"
                                    VerticalContentAlignment="Center"
                                    HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                    VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                    Padding="5, 0" BorderThickness="0" Margin="2,1,0,0" />
                                <xctk:ColorPicker x:Name="CB" Grid.Column="1" Width="48" Visibility="Collapsed"
                                                  SelectedColor="{Binding Color}"
                                                  ShowRecentColors="True" ShowDropDownButton="False" ShowStandardColors="False"/>
                                <!--<ccon:zButton x:Name="CB" OpacityMask="{DynamicResource Icon_Edit}"
                                              Width="16" Height="16" Panel.ZIndex="19"
                                              Background="{Binding Color, Converter={StaticResource ColorToSolidColorBrushConverter}}" BorderThickness="0"
                                              Visibility="Collapsed" Margin="2" Grid.Column="1"/>-->
                                <ccon:zButton x:Name="DB" OpacityMask="{DynamicResource Icon_MinusSign}"
                                              Command="{Binding RemoveThis}"
                                              Width="16" Height="16" Background="#FFD12929" BorderThickness="0"
                                              Visibility="Collapsed" Margin="2" Grid.Column="2"/>
                            </Grid>
                        </Border>
                        <ControlTemplate.Triggers>
                            <!--<DataTrigger Binding="{Binding IsEdited}" Value="true">
                                <Setter Property="FocusManager.FocusedElement" Value="{Binding ElementName=TB}" />
                            </DataTrigger>-->
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="IsMouseOver" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="#1F26A0DA"/>
                                <Setter Property="BorderBrush" TargetName="Bd" Value="#A826A0DA"/>
                            </MultiTrigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="Selector.IsSelectionActive" Value="False"/>
                                    <Condition Property="IsSelected" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="#3DDADADA"/>
                                <Setter Property="BorderBrush" TargetName="Bd" Value="#FFDADADA"/>
                            </MultiTrigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="Selector.IsSelectionActive" Value="True"/>
                                    <Condition Property="IsSelected" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="#3D26A0DA"/>
                                <Setter Property="BorderBrush" TargetName="Bd" Value="#FF26A0DA"/>
                            </MultiTrigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="IsMouseOver" Value="True"/>
                                    <Condition Property="IsSelected" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Visibility" TargetName="Lb" Value="Collapsed"/>
                                <Setter Property="Visibility" TargetName="TB" Value="Visible"/>
                                <Setter Property="Visibility" TargetName="DB" Value="Visible"/>
                                <Setter Property="Visibility" TargetName="CB" Value="Visible"/>
                            </MultiTrigger>
                            <DataTrigger Binding="{Binding Color, Converter={StaticResource BindingExists}, FallbackValue=false}" Value="false">
                                <Setter Property="Visibility" TargetName="CB" Value="Collapsed"/>
                            </DataTrigger>
                            <Trigger Property="IsEnabled" Value="False">
                                <Setter Property="TextElement.Foreground" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <ItemsPanelTemplate x:Key="OrientedItemsPanelTemplate">
            <VirtualizingStackPanel IsItemsHost="True"
                Orientation="{Binding Orientation, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ccon:EditableListBox}}}"/>
        </ItemsPanelTemplate>
    </ResourceDictionary>
</ListBox.Resources>

And now it's just a:

<ccon:EditableListBox ItemsSource="{Binding Players}" AddItem="{Binding AddNewPlayer}" Orientation="Horizontal" HorizontalContentAlignment="Center" ScrollViewer.VerticalScrollBarVisibility="Disabled"/>

And Hell Yeagh! I can make it either vertical or horizontal. I can't belive that when I was just learning WPF I've tried to do something that takes so many things to know and be able to write them (Commands, Templates, Triggers, DependencyProperties, INotifyPropertyChanged and many more), that I had no idea even existed. Just a yaer ago :)

Upvotes: 1

adminSoftDK
adminSoftDK

Reputation: 2092

You have to create the button yourself and assign a command (MVVM) or click (code behind) to it. Your listbox itemssource should be observablecollection which will notify the UI anytime you add or remove an item from the listbox. If you know that you want a listbox with a button in many places, then you can create a usercontrol out of these two controls. If you want a button inside the lixtbox or combobox then you have to modify the template of these controls, slightly more complicated.

Upvotes: 1

paparazzo
paparazzo

Reputation: 45096

Just bind to an ObservableCollection and add the value

If that is not what you are looking for please ask a more specific question

Upvotes: 1

Related Questions