Search code examples
c#wpflistboxitemtemplate

Including an item from the itemsource itself in the ItemTemplate of listbox


I have a listbox in my WPF application.

 <ListBox ItemsSource="{Binding ButtonsCollection}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Border BorderBrush="Black" BorderThickness="2" >
                **Here I want to insert the current button**
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

In my viewmodel, I have a collection of buttons, a property named ButtonsCollection. Lets say: Button with content of "a", Button with content of "b", Button with content of "c".

Now I want the listbox to display each of these buttons with a border as I've declared in the ItemTemplate.


Solution

  • The DataTemplate is instantiated for each item in the ItemsControl (ListBox in your case). Its sole role is to describe how your items would look like when rendered.

    The DataContext should contain objects describing the state of your UI.

    That's a separation of concerns. In this way the UI and the back-end can be developed independently by several people, the DataContext being the contract.

    Of course, as Thomas Christof has pointed out, the framework does not force you to do it like that in any way.

    If you do it like this:

     <ListBox ItemsSource="{Binding ButtonsCollection}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Border BorderBrush="Black" BorderThickness="2" >
                    <ContentControl Content={Binding}"/>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    

    it may work, but as soon as you bind the same collection to another ItemsControl, it would crash. Why? Because every FrameworkElement in WPF must have exactly one parent. WPF will throw an exception "Element is already the child of another element."

    If you follow the MVVM pattern and encapsulate the logical representation of your button in a class:

    public class NamedCommand : ICommand
    {
        private Action _action;
    
        public string Name { get; private set; }
    
        public NamedCommand(string name, Action action)
        {
            Name = name;
            _action = action;
        }
    
        public virtual bool CanExecute(object parameter)
        {
            return true;
        }
    
        public void Execute(object parameter)
        {
            if (_action != null)
                _action();
        }
    
        // Call this whenever you need to update the IsEnabled of the binding target
        public void Update()
        {
            if (CanExecuteChanged != null)
                CanExecuteChanged(this, EventArgs.Empty);
        }
    
        public event EventHandler CanExecuteChanged;
    }
    

    you can bind the same collection to multiple controls, e.g. a menu bar, context menu, side panels, etc.

    In your case that's a ListBox:

     <ListBox ItemsSource="{Binding Commands}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Border BorderBrush="Black" BorderThickness="2" >
                    <Button Content="{Binding Name}" Command="{Binding}"/>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    

    Another problem, which you gonna run into using your current approach is when a background thread tries to manipulate your buttons. UI elements are associated with the thread (STA thread), which created them. You'll end up wrapping all of your calls in Dispatcher.Invokes, possibly deadlocking at some point. Implementing INotifyPropertyChanged, however, and raising the PropertyChanged when required will put this burden on the WPF framework (update notifcations are dispatched on the main thread under the hood).

    As a final note, creating the UI in the code behind is not always a bad idea. Imagine that you want to implement a plugin system in your application and reserve a collapsable region in your layout, which will host the unknown plugin's UI. You can't force the developer of the plugin to have exactly 2 buttons and a text box, so that it will fit well with your DataTemplate. One good solution would be to place a ContentControl in the reserved space and provide an interface for the developer to implement, containing a object GetUI(); method and have a call like this: ContentControl.Content = ActivePlugin.GetUI();