-2

I'm using C# and Blend and working on a WPF application. I'm trying to create my own control that lists out items - e.g. file names. I'm using a StackPanel and each file name is a custom UserControl. What I'm after, is that when a UserControl within the StackPanel.Children is clicked, that UserControl becomes highlighted (a predefined visual state) and all the others become unhighlighted. I've got the Click event within the UserControl-side code, but am stuck trying to implement it to tell all the other UserControls to become unlighted. I've added a publicly accessible property to the UserControl called Selected so was thinking that perhaps the Main program could change that, and then have UserControl code that detects when this changes, and changes it's visual state to unhighlighted. It's essentially a list box with Multi-select turned off! Thanks

Edit

private void UserControl_MouseDown(object sender, MouseButtonEventArgs e)
    {
        if (Selected == false)
        {
            VisualStateManager.GoToState(this, "Highlighted", true);
            Selected = true;
        }

        else
        {
            VisualStateManager.GoToState(this, "Normal", true);
            Selected = false;
        }
    }
Char
  • 41
  • 10

1 Answers1

2

Here's a quick demonstration of how you can completely change the appearance and behavior of list box items in XAML only. All the C# code here is just defining and populating the collections displayed in the list boxes. There are two collections in two different list boxes in order demonstrate the value of writing a generic (generically generic, not specifically Generic in the Foo<T> sense) item container template which is agnostic about what kind of content you're putting in it.

Really, it's a very basic primer in how you think about UI programming in XAML/MVVM. MVVM is a different way of thinking. Like OOP, it seems arbitrary until you grok, and then it's seen to be very powerful.

If you contemplate this code until you understand everything it's doing, you will have taken your first step towards enlightenment. All of this code is rote, paint-by-numbers stuff. Once you master the concepts, applying them in any remotely normal case is not challenging. This lets you save your brain cells for stuff that really is challenging.

First, we'll write a quick viewmodel with collections of like, things and stuff.

ViewModels.cs

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace CustomListBox
{
    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] String propName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
        }
    }

    public class ThingViewModel : ViewModelBase
    {
        #region Name Property
        private String _name = "";
        public String Name
        {
            get { return _name; }
            set
            {
                if (value != _name)
                {
                    _name = value;
                    OnPropertyChanged();
                }
            }
        }
        #endregion Name Property
    }

    public class MainViewModel : ViewModelBase
    {
        #region Things Property
        private ObservableCollection<ThingViewModel> _things = new ObservableCollection<ThingViewModel>();
        public ObservableCollection<ThingViewModel> Things
        {
            get { return _things; }
            set
            {
                if (value != _things)
                {
                    _things = value;
                    OnPropertyChanged();
                }
            }
        }
        #endregion Things Property

        #region Stuff Property
        private ObservableCollection<object> _stuff = new ObservableCollection<object>();
        public ObservableCollection<object> Stuff
        {
            get { return _stuff; }
            set
            {
                if (value != _stuff)
                {
                    _stuff = value;
                    OnPropertyChanged();
                }
            }
        }
        #endregion Stuff Property
    }
}

We'll populate the main viewmodel's collections with some arbitrary whatnot and whatever in the codebehind constructor for the main window.

using System;
using System.IO;
using System.Linq;
using System.Windows;
using System.Collections.ObjectModel;

namespace CustomListBox
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            ViewModel.Things = new ObservableCollection<ThingViewModel>(
                Directory.GetFiles("c:\\").Select(fn => new ThingViewModel { Name = fn }));


            ViewModel.Stuff = new ObservableCollection<Object>(
                Enumerable.Range(1, 10).Select(n => new { Blah = Math.Log(n), Foobar = n }));

            ViewModel.Stuff.Insert(0, new { Blah = "Different type, same name", Foobar = "LOL" });
        }

        public MainViewModel ViewModel => (MainViewModel)DataContext;
    }
}

And here's the XAML:

<Window 
    x:Class="CustomListBox.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:CustomListBox"
    mc:Ignorable="d"
    Title="MainWindow" Height="400" Width="600">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    <Window.Resources>
        <Style x:Key="UglySelectionListBox" TargetType="ListBox" BasedOn="{StaticResource {x:Type ListBox}}">
            <Setter Property="ItemContainerStyle">
                <Setter.Value>
                    <Style TargetType="ListBoxItem" BasedOn="{StaticResource {x:Type ListBoxItem}}">
                        <!-- Need this so the whole listbox item is clickable -->
                        <Setter Property="IsHitTestVisible" Value="True" />
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="ListBoxItem">
                                        <Border 
                                            x:Name="BackgroundBorder" 
                                            BorderThickness="2" 
                                            CornerRadius="4"
                                            Margin="0"
                                            Padding="4"
                                            Background="Transparent"
                                            BorderBrush="#01ffffff"
                                            >
                                            <Grid>
                                                <Grid.ColumnDefinitions>
                                                    <ColumnDefinition Width="Auto" />
                                                    <ColumnDefinition Width="*" />
                                                </Grid.ColumnDefinitions>
                                                <Path
                                                    x:Name="SelectionMark"
                                                    Stroke="Black"
                                                    StrokeThickness="2"
                                                    Data="M0,8 L 4,12 L 10,0"
                                                    Visibility="Hidden"
                                                    />
                                                <!-- 
                                                The ContentPresenter presents the Content property of the ListBoxItem.
                                                The ItemTemplate or DisplayMemberPath determine what the Content actually is
                                                -->
                                                <ContentPresenter ContentSource="Content" Grid.Column="1" />
                                            </Grid>
                                        </Border>
                                    <ControlTemplate.Triggers>
                                        <Trigger Property="IsMouseOver" Value="True">
                                            <Setter TargetName="BackgroundBorder" Property="BorderBrush" Value="Silver" />
                                            <Setter TargetName="BackgroundBorder" Property="Background" Value="Gainsboro" />
                                            <Setter TargetName="SelectionMark" Property="Stroke" Value="Silver" />
                                            <Setter TargetName="SelectionMark" Property="Visibility" Value="Visible" />
                                        </Trigger>
                                        <Trigger Property="IsSelected" Value="True">
                                            <Setter TargetName="BackgroundBorder" Property="Background" Value="LightSkyBlue" />
                                            <Setter TargetName="BackgroundBorder" Property="BorderBrush" Value="Red" />
                                            <Setter TargetName="BackgroundBorder" Property="TextElement.FontWeight" Value="Bold" />
                                            <Setter TargetName="SelectionMark" Property="Stroke" Value="Black" />
                                            <Setter TargetName="SelectionMark" Property="Visibility" Value="Visible" />
                                        </Trigger>
                                    </ControlTemplate.Triggers>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <StackPanel 
            Grid.Column="0"
            Orientation="Vertical"
            >
            <!-- 
            ListBox.SelectedValue goes to the selected item, and returns the 
            value of the property named by SelectedValuePath 
            -->
            <TextBlock 
                Text="{Binding SelectedValue, ElementName=StuffBox}" 
                ToolTip="This displays the 'value' of the SelectedItem in the left listbox"
                />
            <ListBox 
                ItemsSource="{Binding Stuff}"
                Style="{StaticResource UglySelectionListBox}"
                DisplayMemberPath="Blah"
                SelectedValuePath="Foobar"
                Background="Beige"
                x:Name="StuffBox"
                />
        </StackPanel>
        <ListBox 
            Grid.Column="1"
            ItemsSource="{Binding Things}"
            Style="{StaticResource UglySelectionListBox}"
            >
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

OK. We see that we can display all kinds of stuff in a ListBox. The ListBoxItem template determines the look and behavior of the ListBoxItem itself, everything but the content. Then we can throw different kinds of content in there depending on what we populate the ListBox with. We can either name a property to be displayed (as in StuffBox), or provide an item DataTemplate that gives us a lot more options -- you can throw all kids of XAML in that template. It can have its own triggers. Anything you like. Try this one for lolz:

<DataTemplate>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding Name}" />
        <ListBox ItemsSource="{Binding Name}" />
    </StackPanel>
</DataTemplate>

String implements IEnumerable<T>. It's a sequence of characters. Therefore, you can use it as an ItemsSource for a ListBox. You can go absolutely berserk with this stuff.

  • Thanks @EdPlunket, this looks great. I will get back to you when I've tried it! – Char Oct 26 '16 at 09:27
  • thanks for this. It helped my XAML out a fair bit. Although what I was after was a fairly code based solution. I know how to change the VisualState of a UserControl when it's clicked, but I don't know how to change the VisualState of that same control when another (identical) UserControl is clicked within the same stack panel! – Char Oct 26 '16 at 21:46
  • 1