Category Archives: WPF

Display item count in WPF ListBox and reorder using ButtonSpinner

I ran into a situation the other day where I needed to be able to display Trips to a user in the order that they were going to be executed and have them be able to move them up and down the list. I liked how Netflix tackled the issue, but wanted to give the user a little more control.

As it were, I could not find a decent solution to the problem. Google and StackOverflow mostly turned up hacks that were sure to fall apart. After much trial and error, I was able to use an IMultiValueConverter that accepted a ListBox and an item in the ListBox. From there, I could do an IndexOf and return that value back to a TextBlock for display.

The second issue of reordering I solved using the ButtonSpinner in the WPF Extended Toolkit. I had to trick it because I wanted the “down” spinner to move the item to the next higher index and the “up” spin to actually move the item down the list (from an index’s point of view). This functionality requires that the binding list is an ObservableCollection that exposes a Move event and also fires all the INotifyPropertyChanged events that WPF uses for UI refreshing.

Note that the ButtonSpinner is a content control and looks a little ugly when used by itself. The trick is to set the Padding to zero.

So now we have a ListBox that displays the current order of the items and allows the user to move the items up and down the list (which changes the order label) using the ButtonSpinners.

WPF ListBox

In my production code, I actually hide the ButtonSpinners until the user mouses over them, but I didn’t want to clutter the code. I’ll save that for a later post.

The key parts of the code are listed below. The entire project can be downloaded here. Remember to unblock after downloading.

The Converter that takes a ListBox and an item in the ListBox:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;
using System.Windows.Controls;

namespace ReorderingListBox
{
    public class ListBoxIndexConverter : IMultiValueConverter
    {
        #region IMultiValueConverter Members

        public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            var item = values[0];

            if (item == null)
            {
                return null;
            }

            var lb = values[1] as ListBox;
            if (lb == null)
            {
                return null;
            }

            //make it 1 based
            var rv = lb.Items.IndexOf(item) + 1;

            //very important because control
            //we are binding to is expecting a string
            return rv.ToString();
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

The XAML with the ListBox and ButtonSpinner:

<Window x:Class="ReorderingListBox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:converters="clr-namespace:ReorderingListBox"
        xmlns:wpf="http://schemas.xceed.com/wpf/xaml/toolkit"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.Resources>
            <!--reference to our multi converter-->
            <converters:ListBoxIndexConverter x:Key="listBoxIndexConverter" />
            <!--this defines how each item in the listbox will layout-->
            <DataTemplate x:Key="placesTemplate">
                <StackPanel Orientation="Horizontal">
                    <!--shows the current order in the listbox-->
                    <TextBlock Width="15" Name="textBlockOrder">
                        <TextBlock.Text>
                            <!--this is a multibinding, we are into the converter-->
                            <!--the current object that we are bound to (a place)-->
                            <!--and the listbox that it lives in-->
                            <MultiBinding Converter="{StaticResource listBoxIndexConverter}">
                                <Binding />
                                <Binding ElementName="listBox" />
                            </MultiBinding>
                        </TextBlock.Text>
                    </TextBlock>
                    <!--control from http://wpftoolkit.codeplex.com/-->
                    <!--note the padding has to be explicitly set-->
                    <!--otherwise it looks horrible more info about that-->
                    <!--http://timhibbard.com/blog/2012/05/08/default-padding-issue-with-buttonspinner/-->
                    <wpf:ButtonSpinner Padding="0"
                                       Margin="3"
                                       Spin="spinner_Spin" />
                    <TextBlock Text="{Binding}" />
                </StackPanel>
            </DataTemplate>
        </Grid.Resources>
        <ListBox Name="listBox"
                 ItemTemplate="{StaticResource placesTemplate}" />

    </Grid>
</Window>

The code behind that constructs the list that binds to the ListBox and the event that listens to the ButtonSpinner.Spin event:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
using Xceed.Wpf.Toolkit;

namespace ReorderingListBox
{
    ///
<summary> /// Interaction logic for MainWindow.xaml
 /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            //construct our backing list to bind to
            var placesIWantToLive = new ObservableCollection
            {
                "Minneapolis",
                "San Diego",
                "Charleston",
                "Philadelphia"
            };

            this.listBox.ItemsSource = placesIWantToLive;
        }

        private void spinner_Spin(object sender, SpinEventArgs e)
        {
            ButtonSpinner spinner = sender as ButtonSpinner;
            //this will find the textblock in the same listbox item
            TextBlock textOrder = spinner.FindName("textBlockOrder") as TextBlock;
            ObservableCollection places = this.listBox.ItemsSource as ObservableCollection;
            //back to zero based - remember we store the current index in this textblock
            int current = int.Parse(textOrder.Text) - 1;
            int destination = 0;
            switch (e.Direction)
            {
                case SpinDirection.Decrease:
                    //because we want it to go "down" the list
                    destination = current + 1;
                    break;
                case SpinDirection.Increase:
                    //because we want it to go "up" the list
                    destination = current - 1;
                    break;
                default:
                    //we'll never hit here, but else statements are ugly
                    //and not as obvious as switch statements
                    break;
            }
            if (destination < 0)             
            {                 
                 //can't more the first item any higher so exit                 
                 return;             
            }             
            if (destination > places.Count + 1)
            {
                //can't move it any more down so exit
                return;
            }

            //this is where the magic happens
            places.Move(current, destination);
            //this refreshes the textblocks holding the index
            this.listBox.Items.Refresh();
            //highlight the item that was moved so it is obvious to the user
            this.listBox.SelectedIndex = destination;
        }
    }
}

Default Padding issue with ButtonSpinner

The Extended WPF Toolkit plugs a large hole in the WPF framework and we use it extensively at EnGraph. Every now and then, you find a little odd thing. I ran into an issue this morning where the ButtonSpinner looked very odd if you didn’t specify any child content to be inside the spinner.

It’s not really a bug because the control was designed to have content, but I needed it to be by itself.

<wpf:ButtonSpinner />

It produced that ugly border to the left of the control where the content would be. Digging through the properties, I found that the default value for Padding was set to 2. So I changed that to be 0 and now we get the excepted result:

<wpf:ButtonSpinner Padding="0" />


Perfect.

Using ItemContainerStyle to manage individual items of a bound CheckListBox

This post uses the CheckListBox in the excellent Extended WPF Toolkit library (originally created by Brian Lagunas)

Our software supports trips imported by LogistiCare. The user selects a CSV file, we parse it, then they select from a list which trips they wish to import. Sometimes the data in the download just isn’t right and I needed a way to prevent the user from importing bad data into the system.

I could have done this validation in the manager that parses the CSV, but this could have lead to confusion. What if the original download listed 300 trips and we only displayed 280 available to import?

It could also have been done on the Data Access Layer side, but if we displayed 300 trips to import, but only 280 showed up in Scheduling Canvas, the user would also be confused.

So it seemed that the UI would be the logical place to do this. That way the user is aware of what is going on. Now, we don’t actually want the logic to be done in the UI layer, so we’ll do the heavy lifting in the object and just expose a public readonly property called LogisticareIDIsValid that returns a boolean.

We are able to hook into the ItemContainerStyle of the CheckListBox to be able to set properties on each individual item listed. In this case, we want IsEnabled to be bound to LogisticareIDIsValid.

With ItemContainerStyle, you can also cool things like subscribe to events. You can see we subscribed to the MouseDoubleClick event.

This code assumes you know how to declare a namespace in your XAML.

Here is the XAML:

<wpf:CheckListBox>
    <wpf:CheckListBox.ItemContainerStyle>
        <Style TargetType="{x:Type wpf:CheckListBoxItem}">
            <Setter Property="IsEnabled"
                    Value="{Binding Path=LogisticareIDIsValid}" />
            <EventSetter Event="MouseDoubleClick"
                    Handler="listResultsDoubleClick" />
        </Style>
    </wpf:CheckListBox.ItemContainerStyle>
</wpf:CheckListBox>

I had originally accomplished this using a method and a converter. I didn’t really see how much easier it was to use a property until I was getting ready to publish this post and the mistake was glaring. Funny how having to explain your thought process exaggerates the error of your ways.

WPF ComboBox initial selection

I ran into a scenario where I needed a WPF ComboBox to be pre-populated when loaded. I couldn’t use SelectedIndex = 1 because I didn’t know if the ComboBox would have any items in it. If there were not any items in the list, it needed to be unselected (SelectedIndex = -1). So I bound the SelectedIndex to the Items.Count property and ran it through a converter.

By interrogating the Items.Count, we can tell if the ComboBox has items in it. If so, we can set the SelectedIndex to be 0 – which makes the first item selected. If Items.Count is 0, we set the SelectedIndex to be -1 – which means no selection.

The analysis happens in the converter. Much like the name suggests, it takes a value, and converts it to a different value. It is a powerful way of changing the behavior of the UI, without cluttering up the code behind. Converters are also decoupled from the XAML and can be used by multiple windows.

Some other converters I use on a regular basis are:
BooleanToVisibleVisiblity – Good way to show an Element if CheckBox.IsChecked = true
ZeroCollapsed – Good way to hid a Listbox if it doesn’t contain any items

Here is the XAML:

<ComboBox ItemsSource="{Binding Source={StaticResource clientProgramManager_GetAll}}"
          SelectedIndex="{Binding RelativeSource={RelativeSource Self},
                         Path=Items.Count,
                         Converter={StaticResource listCountToSelectedIndex},
                         Mode=OneTime}"/>

Here is the converter code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;

namespace ParaPlan.Converters
{
    public class ListCountToSelectedIndex : IValueConverter
    {
        #region IValueConverter Members

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            var listCount = 0;
            int.TryParse(value.ToString(), out listCount);
            if (listCount == 0)
            {
                //no items, make selected index - 1
                return -1;
            }
            //has items, make selected index the first one in the list
            return 0;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

WPF TimeSpinner Control

spinner

As I discussed in my last post, we created a TimeSpinner control based on the Extended WPF Toolkit’s ButtonSpinner.

Now, the toolkit has a DateTimeUpDown control that will display a date or time and allow the user to click the spinners for each time part, but I needed a little more. I needed the raw text to be editable and I wanted the spinners to just modify the minutes portion. I also wanted to have special parsing based on the number of characters entered.

4 chars:
Parse as military time.

3 chars: 
Parse as modified military time, but only assume waking hours.

So 933 becomes 9:33 AM

345 becomes 3:45 PM

We put the cut-off at 7. Anything before 7 assumes PM anything 7 or after assumes AM.

2 chars:
Assuming shortened military time.

12 = 12:00 PM

15 = 3:00 PM

1 char:
Parses as shortened military time, but assumes waking hours like 3 chars.

1 = 1:00 PM

8 = 8:00 AM

 

The TimeSpinnerControl exposes one bindable property, Time. So it would be consumed like:

   1: <runtime:TimeSpinnerControl Time="{Binding Path=ReturnTime}"

   2:                             x:Name="spinnerReturnTime"/>

 

So, here is what we did:

TimeSpinnerControl.xaml:

   1: <UserControl x:Class="ParaPlan.Controls.TimeSpinnerControl"

   2:              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   3:              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   4:              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 

   5:              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 

   6:              xmlns:wpftoolkit="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit.Extended"

   7:              mc:Ignorable="d">

   8:     <wpftoolkit:ButtonSpinner Spin="Time_Spin" IsTabStop="False">

   9:         <TextBox Name="textTime" 

  10:                  MinWidth="60" 

  11:                  Margin="0"

  12:                  MouseDoubleClick="textTimeDoubleClick"

  13:                  Keyboard.GotKeyboardFocus="textTimeGotKeyboardFocus"

  14:                  MouseEnter="textTimeMouseEnter"

  15:                  Keyboard.LostKeyboardFocus="textTimeLostKeyboardFocus"/>

  16:     </wpftoolkit:ButtonSpinner>

  17: </UserControl>

 

TimeSpinnerControl.xaml.cs:

   1: using System;

   2: using System.Collections.Generic;

   3: using System.Linq;

   4: using System.Text;

   5: using System.Windows;

   6: using System.Windows.Controls;

   7: using System.Windows.Data;

   8: using System.Windows.Documents;

   9: using System.Windows.Input;

  10: using System.Windows.Media;

  11: using System.Windows.Media.Imaging;

  12: using System.Windows.Navigation;

  13: using System.Windows.Shapes;

  14: using ParaPlan.Extensions;

  15: using System.Globalization;

  16:  

  17: namespace ParaPlan.Controls

  18: {

  19:     /// <summary>

  20:     /// Interaction logic for TimeSpinnerControl.xaml

  21:     /// </summary>

  22:     public partial class TimeSpinnerControl : UserControl

  23:     {

  24:         public TimeSpinnerControl()

  25:         {

  26:             InitializeComponent();

  27:         }

  28:  

  29:         #region Time Property

  30:  

  31:         /// <summary>

  32:         /// Time Dependency Property

  33:         /// </summary>

  34:         public static readonly DependencyProperty TimeProperty =

  35:             DependencyProperty.Register("Time", typeof(DateTime), typeof(TimeSpinnerControl),

  36:                 new FrameworkPropertyMetadata(DateTime.MinValue,

  37:                     FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,

  38:                     new PropertyChangedCallback(OnTimeChanged)));

  39:  

  40:         /// <summary>

  41:         /// Gets or sets the Time property.

  42:         /// </summary>

  43:         public DateTime Time

  44:         {

  45:             get { return (DateTime)GetValue(TimeProperty); }

  46:             set { SetValue(TimeProperty, value); }

  47:         }

  48:  

  49:         /// <summary>

  50:         /// Handles changes to the Time property.

  51:         /// </summary>

  52:         private static void OnTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

  53:         {

  54:             ((TimeSpinnerControl)d).OnTimeChanged(e);

  55:         }

  56:  

  57:         /// <summary>

  58:         /// Provides derived classes an opportunity to handle changes to the Time property.

  59:         /// </summary>

  60:         protected virtual void OnTimeChanged(DependencyPropertyChangedEventArgs e)

  61:         {

  62:             DateTime value = (DateTime)e.NewValue;

  63:             if (value.IsMinValue())

  64:             {

  65:                 textTime.Text = "";

  66:                 return;

  67:             }

  68:             textTime.Text = value.ToShortTimeString();

  69:         }

  70:  

  71:         #endregion

  72:  

  73:         #region Event Handlers

  74:         /// <summary>

  75:         /// Handles when the user clicks a spinner

  76:         /// </summary>

  77:         /// <param name="sender"></param>

  78:         /// <param name="e"></param>

  79:         private void Time_Spin(object sender, Microsoft.Windows.Controls.SpinEventArgs e)

  80:         {

  81:             if (e.Direction == Microsoft.Windows.Controls.SpinDirection.Increase)

  82:             {

  83:                 this.Time = this.Time.AddMinutesRoundUp(5);

  84:             }

  85:             else

  86:             {

  87:                 this.Time = this.Time.AddMinutesRoundUp(-5);

  88:             }

  89:  

  90:         }

  91:  

  92:         /// <summary>

  93:         /// Handles when the user double clicks in the textbox

  94:         /// </summary>

  95:         /// <param name="sender"></param>

  96:         /// <param name="e"></param>

  97:         private void textTimeDoubleClick(object sender, MouseButtonEventArgs e)

  98:         {

  99:             this.textTime.SelectAll();

 100:         }

 101:  

 102:         /// <summary>

 103:         /// Handles when the user tabs in the control

 104:         /// </summary>

 105:         /// <param name="sender"></param>

 106:         /// <param name="e"></param>

 107:         private void textTimeGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)

 108:         {

 109:             this.textTime.SelectAll();

 110:         }

 111:  

 112:         /// <summary>

 113:         /// Handles when the user enters the control by mouse

 114:         /// </summary>

 115:         /// <param name="sender"></param>

 116:         /// <param name="e"></param>

 117:         private void textTimeMouseEnter(object sender, MouseEventArgs e)

 118:         {

 119:             this.textTime.SelectAll();

 120:         }

 121:  

 122:         /// <summary>

 123:         /// Handles when the control loses focus. Now we want to parse what

 124:         /// the user entered

 125:         /// </summary>

 126:         /// <param name="sender"></param>

 127:         /// <param name="e"></param>

 128:         private void textTimeLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)

 129:         {

 130:             this.Time = ParseText(this.textTime.Text);

 131:         } 

 132:         #endregion

 133:  

 134:         private DateTime ParseText(string value)

 135:         {

 136:             //empty...move on

 137:             if (string.IsNullOrWhiteSpace(value))

 138:             {

 139:                 return DateTime.MinValue;

 140:             }

 141:             

 142:             //parsed as valid date

 143:             DateTime date = DateTime.MinValue;

 144:             DateTime.TryParse(value, out date);

 145:             if (!date.IsMinValue())

 146:             {

 147:                 return date;

 148:             }

 149:  

 150:             string input = value;

 151:             int first = int.Parse(value.Substring(0, 1));

 152:             switch (value.Length)

 153:             {

 154:                 case 3:

 155:                     input = "0" + value;

 156:                     if (first > 0 && first < 7)

 157:                     {

 158:                         input = (first + 12).ToString() + value.Substring(1, 2);

 159:                     }

 160:                     break;

 161:                 case 2:

 162:                     input = value + "00";

 163:                     break;

 164:                 case 1:

 165:                     input = "0" + value + "00";

 166:                     if (first > 0 && first < 7)

 167:                     {

 168:                         input = (first + 12).ToString() + "00";

 169:                     }

 170:                     break;

 171:                 default:

 172:                     break;

 173:             }

 174:             DateTime.TryParseExact(input, "HHmm", null, DateTimeStyles.None, out date);

 175:             return date;

 176:         }

 177:  

 178:  

 179:     }

 180: }

 

DateTime Extension Method:

   1: /// <summary>

   2: /// Adds or subtracts a value to a date time rounding it to

   3: /// multiples of the entered value

   4: /// </summary>

   5: /// <param name="dt">this</param>

   6: /// <param name="value">Amount to add or subtract</param>

   7: /// <returns>Adjusted DateTime</returns>

   8: /// <history>

   9: ///     [Tim Hibbard]   04/08/2011  Created

  10: /// </history>

  11: public static DateTime AddMinutesRoundUp(this DateTime dt, int value)

  12: {

  13:     int delta = dt.Minute % value;

  14:     int subtractDeltaFrom = 0;

  15:     //since value to add is negative, we want to round down

  16:     //to nearest delta value

  17:     if (value < 0)

  18:     {

  19:         //but only if delta is more than 0. If delta is 0, 

  20:         //then we are already rounded down

  21:         if (delta > 0)

  22:         {

  23:             subtractDeltaFrom = Math.Abs(value); 

  24:         }

  25:         

  26:     }

  27:     return dt.AddMinutes(subtractDeltaFrom - delta).AddMinutes(value);

  28: }



Smarter DateTime.AddMinutes

times

Our software will automatically generate pick up and drop off times based on distance of trip, how many other people are on the bus, how long it takes to drop them off, etc. We display these times in our custom TimeSpinner control based on WPF Extended Toolkit’s button spinner.

Since the computer is generating the time, they are often not human-friendly. 6:00 AM is a lot easier to remember than 6:03 AM. So we give the users the option to modify these times. Using the spinners, they can adjust the time 5 minutes up or 5 minutes down. However, 6:08 is still difficult to remember, so what we really need is to be able to round up (or down) to the nearest 5 minutes, then adjust in 5 minute increments after that.

So 6:03 AM to 6:05 AM to 6:10 AM to 6:15 AM and likewise, round down from 6:03 AM to 6:00 AM to 5:55 AM to 5:50 AM.

We used an extension method to accomplish this. Note that it accepts increments other than 5 minutes, that’s just what we use.

   1: /// <summary>

   2: /// Adds or subtracts a value to a date time rounding it to

   3: /// multiples of the entered value

   4: /// </summary>

   5: /// <param name="dt">this</param>

   6: /// <param name="value">Amount to add or subtract</param>

   7: /// <returns>Adjusted DateTime</returns>

   8: public static DateTime AddMinutesRoundUp(this DateTime dt, int value)

   9: {

  10:     int delta = dt.Minute % value;

  11:     int subtractDeltaFrom = 0;

  12:     //if value to add is negative, we want to round down

  13:     //to nearest delta value

  14:     if (value < 0)

  15:     {

  16:         //but only if delta is more than 0. If delta is 0, 

  17:         //then we are already rounded down

  18:         if (delta > 0)

  19:         {

  20:             subtractDeltaFrom = Math.Abs(value); 

  21:         }

  22:         

  23:     }

  24:     return dt.AddMinutes(subtractDeltaFrom - delta).AddMinutes(value);

  25: }



WPF – Show Dialog with backdrop

dialog

Sometimes it handy to force the user’s attention to a specific screen. We can do this with a dialog, but sometimes the user doesn’t know that window is on top. To draw the user’s eye to the window, I like to place a backdrop between the rest of the application and the dialog window. I use the following code to make that happen.

   1: public bool? ShowDialogWithBackdrop(Window win)

   2: {

   3:     //create backgroup

   4:     Window backgroundWindow = new Window();

   5:     backgroundWindow.Top = 0;

   6:     backgroundWindow.Left = 0;

   7:     backgroundWindow.Height = SystemParameters.WorkArea.Height;

   8:     backgroundWindow.Width = SystemParameters.WorkArea.Width;

   9:     backgroundWindow.AllowsTransparency = true;

  10:     backgroundWindow.Background = Brushes.Transparent;

  11:     backgroundWindow.WindowStyle = WindowStyle.None;

  12:     backgroundWindow.ShowInTaskbar = false;

  13:     Rectangle rect = new Rectangle() { Fill = Brushes.Silver, Opacity = 0.5 };

  14:     backgroundWindow.Content = rect;

  15:     backgroundWindow.Show();

  16:     //set the owner so if the user clicks on the backdrop,

  17:     //the window will flash

  18:     win.Owner = backgroundWindow;

  19:     bool? rv = win.ShowDialog();

  20:     //dialog has returned. Close backdrop.

  21:     backgroundWindow.Close();

  22:     return rv;

  23: }