A ListBox for strings that supports add and delete

Ok, I was quite annoyed when I could not bind to an ObservableCollection and be able to add and remove rows in a DataGrid. I just wanted a list of strings to display and to easily update them.

I was sad that this does’t exist by default, so I created one. This is actually a UserControl that hosts a ListBox, but it works just like a ListBox. I can add to and delete from a bound ObservableCollection.

I wanted it to support pressing delete. Right-clicking and choosing delete. Typing in a new item at the bottom and clicking Add. Or click enter after typing.

I wanted it to work with List too in that if an item is added from the UI, the UI and the List are updated and I got that to work. However, if the List is updated in code and not via the ui, the UI will not know about this change. That is what ObservableObject is for.

<UserControl x:Class="AddRemoveStringList.StringListBox"
             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" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Label Name="TitleLabel" Content="{Binding Title}" Grid.ColumnSpan="2"/>
        <ListBox x:Name="Box" Grid.Row="1" Grid.ColumnSpan="2" KeyDown="OnDeletePressed" ItemsSource="{Binding ItemsSource}">
            <ListBox.Resources>
                <ContextMenu x:Key="ContextMenuDelete">
                    <MenuItem Header="_Delete" Click="MenuDeleteClicked" />
                </ContextMenu>
            </ListBox.Resources>
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
                    <Setter Property="ContextMenu" Value="{DynamicResource ContextMenuDelete }">
                    </Setter>
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>
        <TextBox Name="AddStringBox" Grid.Row="2" KeyDown="OnEnterPressed" />
        <Button Name="AddStringButton" Content="Add" Grid.Row="2" Grid.Column="1" Click="AddClicked" MinWidth="30" />
    </Grid>
</UserControl>
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace AddRemoveStringList
{
    /// <summary>
    /// Interaction logic for StringListBox.xaml
    /// </summary>
    public partial class StringListBox : UserControl
    {
        public StringListBox()
        {
            InitializeComponent();
            Box.DataContext = this;
            TitleLabel.DataContext = this;
        }

        #region Events
        private void AddClicked(object sender, RoutedEventArgs e)
        {
            ItemsSource.Add(AddStringBox.Text);
            AddStringBox.Clear();
            object o = this.DataContext;
            if (!(o is INotifyCollectionChanged))
            {
                this.DataContext = null;
                this.DataContext = o;
            }
        }

        private void MenuDeleteClicked(object sender, RoutedEventArgs e)
        {
            ItemsSource.Remove(Box.SelectedItem.ToString());
        }

        private void OnDeletePressed(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Delete)
            {
                MenuDeleteClicked(sender, e);
            }
        }

        private void OnEnterPressed(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter)
            {
                AddClicked(this, e);
            }
        }
        #endregion

        #region ItemsSource DependencyProperty
        public ICollection<string> ItemsSource
        {
            get { return (ICollection<string>)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource", typeof(ICollection<string>), typeof(StringListBox), null);
        #endregion

        #region Title DependencyProperty
        public string Title
        {
            get { return (string)GetValue(TitleProperty); }
            set { SetValue(TitleProperty, value); }
        }

        public static readonly DependencyProperty TitleProperty =
            DependencyProperty.Register("TitleProperty", typeof(string), typeof(StringListBox), null);
        #endregion
    }
}

And here is how you use it just like you would a ListBox.

<Window x:Class="AddRemoveStringList.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:loc="clr-namespace:AddRemoveStringList"
        Title="MainWindow" Height="350" Width="525"
        >
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <loc:StringListBox x:Name="ListViewStringList1" ItemsSource="{Binding StringList1}"  />
        <ListBox x:Name="ListViewStringList2" ItemsSource="{Binding StringList2}" Grid.Row="1" />
    </Grid>
</Window>

For those who wonder why I used code-behind and question if this is breaking MVVM, it is not. I am creating a reusable control here. When creating a control, the goal is to have everything in one object. Everything in the code behind is related to the UI code or uses an interface. You can use this and bind to it in MVVM just like you would a ListBox.

3 Comments

  1. sergio says:

    This coede will never work. There is no Add property to ItemsSource object.

    • Rhyous says:

      5 years ago, this code worked perfectly. Perhaps this property solves your concern:

              public ICollection<string> ItemsSource
              {
                  get { return (ICollection<string>)GetValue(ItemsSourceProperty); }
                  set { SetValue(ItemsSourceProperty, value); }
              }
      
  2. Patrick says:

    Awesome! Thank you. I needed something quick and simple.

Leave a Reply