Click here to Skip to main content
15,884,388 members
Articles / Desktop Programming / WPF

An Editable ListBox for WPF

Rate me:
Please Sign up or sign in to vote.
4.83/5 (7 votes)
13 May 2009CPOL19 min read 78.2K   3K   23   2
A simple to implement editable listbox for WPF.

Introduction

Here's an approach for displaying and editing data that you might find useful. It doesn't replace the venerable data grid, but I think it's simpler to implement, and might be a better fit for some applications. Since the controlling element used with this approach is a listbox, it is more suited for smaller number of fields and a manageable dataset size. But it could also be expanded with some form of filtered paging to enable larger datasets.

I think this approach, with its layered design, represents the separation of concerns. And although it was not intuitive at first (for me), the end result has nothing particularly special about it. So there is a minimal amount of change required for the implementation.

A Prerequisite Note

I'm using the Composite Application Library (CAL) with the demo application. However, it is not a requirement for building an editable listbox. I'm using the library because I'm currently evaluating it, so it is more for convenience than anything else. There are two things that are required for any design that implements a separated presentation pattern: an implementation for commanding, and some mechanism to tie the View to the Presentation Model. CAL provides an implementation of ICommand in its CommandDelegate class. This facilitates the loose coupling that we want between the UI and the supporting logic. CAL also includes a dependency injection container, which I'm using to make the connection between the View and the Presentation Model in the form of a view injection. Both of these requirements can be achieved in other ways instead of using the library. In the References section at the end of the article, I've indicated sources for alternate methods.

Editable Listbox?

Creating an editable listbox is pretty easy. You just have to create a user control to use as the rendering for the items, define a template for the ListBoxItem in order to set the user control as the content, and then just plop it into the listbox as the DataTemplate.

XML
<Control.Resources>
    ...
    <Style TargetType="{x:Type ListBoxItem}">
        <Setter Property="ContentTemplate">
            <Setter.Value>
                <DataTemplate>
                    <Border BorderBrush="Black"
                            BorderThickness="1"
                            Margin="2">
                        <StackPanel Orientation="Horizontal">
                            <ctl:WeebitItemCtl/>
                        </StackPanel>
                    </Border>
                </DataTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    ...
</Control.Resources>
...
<ListBox IsSynchronizedWithCurrentItem="True" 
             Name="listWeebits"
             ItemsSource="{Binding Path=WeebitListItems}"
             SelectedItem="{Binding Mode=TwoWay, Path=FocusWeebitItem}"
             Grid.ColumnSpan="5">
</ListBox>
...

If you do that, you'll get a listbox that will be editable, but it won't be very useful. The user can edit each item, but you won't have control over it.

First_Try.PNG

Fig. 1 First Try

So that's really the challenge here, managing it so that it is useful and intuitive at the same time. In other words, behaves as usual.

The first hurdle is restricting what the user can edit. I started by making the items disabled except for the one selected for editing. That didn't work too well for me because you lose a lot of information when an item is in disabled mode. It becomes less readable, and as you can see, the color information is lost, which might be what the user needs in order to make the selection. And of course, it looks strange seeing all the items disabled.

Disabled_View.PNG

Fig. 2 Disabled View

Associated with this is the problem of selecting an item. Since the user interaction is with the user control, the listbox has no way to manage the selection. The only way to 'select' an item in the listbox is if you leave an area around the user control that the user can click on. That's not a good mechanism. In Fig. 1 above, you can see that the user selected an item but was also able to make changes to a different item.

So, here's the challenge:

  • We need to somehow restrict the user's interaction to only the item being edited. And only one item can be editable at a time.
  • We need to provide some form of intuitive selection mechanism so the user can select an item for editing.
  • Once an item has been selected, we need some feedback to indicate to the user the item currently 'selected' for editing.
  • We would also like to provide some visual cues to the user regarding the validity of the data entered.
  • And, we'd like to implement most of this in XAML.

That's it as far as the listbox is concerned. Of course, we need additional functionality that will allow the user to save the changes or cancel the operation. But, that's external (to the listbox) functionality.

The Data

Let's start by defining the data that I'm using in the demo project. The following defines the data object that we're dealing with. Just something with a little variation in property types:

WeebitData.PNG

Fig. 3 Weebit Data

We have a WeebitData object that has a name, description, physical dimensions, a color, and a type as properties. The class definition is a simple properties interface. The only interesting aspect of the class is that it also supports validation of the properties (data). By implementing IDataErrorInfo, it can be hooked up directly to the UI and provide validation for the user entries. It's pretty interesting to see how much I get with very little work on my part. Granted, the validation presented here for the demo is pretty minimal. Here's a peek at that part of the implementation for the class:

C#
...
#region IDataErrorInfo

string IDataErrorInfo.Error { get { return null; } }

string IDataErrorInfo.this[string propertyName]
{
    get { return this.GetValidationError(propertyName); }
}

#endregion
#region validations
string GetValidationError(string propertyName)
{
    string error = null;

    switch (propertyName)
    {
        case "Name":
            error = this.ValidateName();
            break;
        case "Description":
            error = this.ValidateDescription();
            break;
        case "Length":
            error = this.ValidateFloat(this.Length,"Length");
            break;
        case "Width":
            error = this.ValidateFloat(this.Width,"Width");
            break;
        case "Height":
            error = this.ValidateFloat(this.Height,"Height");
            break;
    }

    return error;
}
string ValidateName()
{
    if (String.IsNullOrEmpty(this.Name))
    {
        return "Enter Name";
    }
    return null;
}
string ValidateDescription()
{
    if (String.IsNullOrEmpty(this.Description))
    {
        return "Enter Description";
    }
    return null;
}
string ValidateFloat(string value, string item)
{
    if (String.IsNullOrEmpty(value))
    {
        return "Enter "+item;
    }
    try
    {
        float temp = float.Parse(value);
        return null;
    }
    catch
    {
        return "Invalid entry.";
    }
}
#endregion

Pretty simple. The name of the property is used as an indexer, and after evaluating the validity of the property, return back null if it's valid, or an error description otherwise. We'll see how it gets hooked up when we look at the UI. The WeebitService class provides access to the repository, and has your typical access methods. There is really nothing particularly interesting going on in there, so we'll move on (the downloadable project has the complete source).

Weebit Item UserControl

The user control used to render the Weebit data is depicted in Fig. 2 above, and the XAML for it is shown below. It's a pretty simple control comprised of a few textboxes, a combobox, and a button.

XML
<UserControl x:Class="EditableListboxApp.Modules.Weebit.Views.WeebitItemCtl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Converters="clr-namespace:EditableListboxApp.Infrastructure.
                      Converters;assembly=EditableListboxApp.Infrastructure"
    Height="Auto" Width="Auto">
    <UserControl.Resources>
        <Converters:ColorToSolidColorBrushConverter 
          x:Key="colorToSolidColorBrushConverter" />
        <Style x:Key="tbWithValidation" TargetType="{x:Type TextBox}">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip"
                Value="{Binding RelativeSource={RelativeSource Self}, 
                       Path=(Validation.Errors)[0].ErrorContent}"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </UserControl.Resources>
    <Grid x:Name="ctlWnd" Height="Auto" Width="Auto">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="60"></ColumnDefinition>
            <ColumnDefinition Width="100"></ColumnDefinition>
            <ColumnDefinition Width="40"></ColumnDefinition>
            <ColumnDefinition Width="40"></ColumnDefinition>
            <ColumnDefinition Width="40"></ColumnDefinition>
            <ColumnDefinition Width="80"></ColumnDefinition>
            <ColumnDefinition Width="50"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <TextBox Name="textName" IsEnabled="{Binding Path=IsNameEnabled}" 
                 Style="{StaticResource tbWithValidation}" 
                 Text="{Binding Path=Name, ValidatesOnDataErrors=True, 
                       UpdateSourceTrigger=PropertyChanged}" />
        <TextBox Name="textDescription" 
                 Style="{StaticResource tbWithValidation}" Grid.Column="1"  
                 Text="{Binding Path=Description, ValidatesOnDataErrors=True, 
                       UpdateSourceTrigger=PropertyChanged}"/>
        <TextBox Grid.Column="2" Name="textLength" 
                 Style="{StaticResource tbWithValidation}" 
                 Text="{Binding Path=Length, ValidatesOnDataErrors=True, 
                       UpdateSourceTrigger=PropertyChanged}"/>
        <TextBox Grid.Column="3" Name="textWidth" 
                 Style="{StaticResource tbWithValidation}" 
                 Text="{Binding Path=Width, ValidatesOnDataErrors=True, 
                       UpdateSourceTrigger=PropertyChanged}"/>
        <TextBox Grid.Column="4" Name="textHeight" 
                 Style="{StaticResource tbWithValidation}" 
                 Text="{Binding Path=Height, ValidatesOnDataErrors=True, 
                       UpdateSourceTrigger=PropertyChanged}"/>
        <Button Grid.Column="6" Name="buttonColor" 
                Background="{Binding Converter={StaticResource 
                            colorToSolidColorBrushConverter}, Path=WeebitColor}"
                Command="{Binding Path=ColorButtonCommand}">
            Color...
        </Button>
        <ComboBox Grid.Column="5" 
           ItemsSource="{Binding Path=WeebitTypes, Mode=OneTime}" 
           Name="comboType" SelectedItem="{Binding Path=GSWeebitType}" />
    </Grid>
</UserControl>

There're a couple of items above that probably require some explanation. First, the need for the Converter. The color information stored for the Weebit is stored as a color, but to render the background of the button, we need a brush. The Converter provides the necessary 'conversion' from value to visual (the conversion could also have been implemented in the Presentation Model, but it was cleaner for me this way). Second, 'tbWithValidation' defines a trigger for the TextBoxes that indicates how we want to display validation errors. When there is a validation error, we want to display the validation error as a tooltip text. And thirdly, you might wonder why the name field is singled out for disabling. The name is the key field for the data. When the user is editing an existing item, we definitely don't want them to change that field. But when the user is creating a new entry, we have to allow access. So here is an area where validation could be expanded. The name could be checked as it's being entered, and if it's not unique, an appropriate error message could be returned for the validation error.

Each of the items above has the ValidatesOnDataErrors set to 'true' in order to hook in the validation check. And, we want to evaluate every time on the PropertyChanged event (yeah, I took a shortcut, I am not validating the type or color, it would only be necessary for new entries anyway). Finally, all of the items are bound to the WeebitPresentationModel, which we'll look at next.

WeebitPresentationModel

The WeebitPresentationModel class is the enabler for the View (user control). It is the source for the bindings defined in the View. So, it must, at a minimum, provide the endpoints for the data bindings. And, in order for it to provide all the UI magic that takes place underneath, it has to implement INotifyPropertyChanged to keep the UI synchronized with the data by initiating UI updates. To me, this is pretty neat. It's like blind communication, since you really don't know who your talking to. Finally, to provide the data validation, it needs to implement IDataErrorInfo, even though, in this case, it is just a pass through.

C#
public class WeebitPresentationModel : INotifyPropertyChanged, IDataErrorInfo
{
    WeebitData weebitData;

    readonly ICommand colorButtonCommand;

    bool isEnabled = false;
    bool isNameEnabled = true;

    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    public WeebitPresentationModel(WeebitData weebitData)
    {
        if (weebitData == null)
            throw new ArgumentNullException("WeebitData");

        this.weebitData = weebitData;

        colorButtonCommand = new DelegateCommand<string>(OnColor);
    }
    #region weebit properties
    public string Name
    {
        set
        {
            weebitData.Name = value;
            this.OnPropertyChanged("Name");
            CommandManager.InvalidateRequerySuggested();
        }
        get { return weebitData.Name; }
    }

The class is initialized with a reference to a WeebitData object so that it can support the View. For this application, I think this is the simplest way since each listbox item is really a View with its supporting Presentation Model. WeebitPresentationModel passes all responsibility relating to the data to the WeebitData object. Its concern is mainly with supporting the View requirements. In the constructor, we also initialize a DelegateCommand for the Weebit color button. As I mentioned previously, this is one of the facilities that I am utilizing from CAL. The DelegateCommand takes two parameters, both methods. One is the method to be called to determine the state of the UI control. And, the second is the method to call when executing the command. In the instance we are looking at, the color button, it only requires one parameter since the button will always be enabled.

All the properties are handled in much the same way as the Name property shown above. The data is passed through to the WeebitData object and retrieved from the WeebitData object. Any time the data changes, notification is propagated up to the UI through the OnPropertyChanged event. The use of the CommandManager here requires a little explanation. It's actually something that is not supported with the DelegateCommand implementation provided with CAL. Here's what I needed. Since we are validating the input from the user, we need to be able to check and change the state of the UI (buttons) as each property is being changed, essentially each keystroke. Specifically, I want to disallow saving the data if any of the properties are invalid. What the InvalidateRequerySuggested call is doing is telling the CommandManager to invalidate the state of the commanding controls that it is monitoring. That will trigger a call to the methods, which we will describe later, that determine the state of our buttons. Clear as mud? In any case, I had to make a modification to the CommandDelegate class in order to get this to work. As I mentioned previously, you don't have to use CAL, the Reference section provides alternatives to the CommandDelegate.

C#
#region UI
public bool IsItemEnabled
{
    get { return isEnabled; }
    set
    {
        isEnabled = value;
        this.OnPropertyChanged("IsItemEnabled");
        CommandManager.InvalidateRequerySuggested();
    }
}
public bool IsNameEnabled
{
    get { return isNameEnabled; }
    set
    {
        isNameEnabled = value;
        this.OnPropertyChanged("IsNameEnabled");
        CommandManager.InvalidateRequerySuggested();
    }
}
public ICommand ColorButtonCommand
{
    get { return colorButtonCommand; }
}
public string[] WeebitTypes
{
    get { return Enum.GetNames(typeof(WeebitType)); }
}
public void OnColor(string notused)
{
    ColorDialog dlg = new ColorDialog();

    if (dlg.ShowDialog() == DialogResult.OK)
    {
        weebitData.WeebitColor = dlg.Color;
        this.OnPropertyChanged("WeebitColor");
    }
}
#endregion

I'll describe the IsItemEnabled property later when we get to the listbox itself. For now, it's just a property that is settable, but it is not tied to the IsEnabled property of the user control. The IsNameEnabled I did describe briefly before, and it does represent the UI state. Both of these will become clearer once we see how they are used.

The WeebitTypes property is used by the ComboBox to get the available options for the user. And, the OnColor method is called when the user selects the color button. The unused parameter is also a consequence of using the CommandDelegate.

C#
#region operations
public bool IsValid()
{
    //All validation is handled by data object...
    return weebitData.IsValid();
}
public bool Save(bool isNew)
{
    if (isNew)
        return weebitService.NewWeebit(weebitData);
    else
        return weebitService.UpdateWeebit(weebitData);

}
/// <summary>
/// We'll just re-load the data
/// </summary>
public bool Cancel()
{
    bool returnVal = false;
    WeebitData temp = weebitData;   //Save it just in case
    weebitData = weebitService.GetWeebit(weebitData.Name);
    if (weebitData != null)
    {
        if (temp.Description != weebitData.Description)
            this.OnPropertyChanged("Description");
        if (temp.Length != weebitData.Length)
            this.OnPropertyChanged("Length");
        if (temp.Width != weebitData.Width)
            this.OnPropertyChanged("Width");
        if (temp.Height != weebitData.Height)
            this.OnPropertyChanged("Height");
        if (temp.WeebitType != weebitData.WeebitType)
            this.OnPropertyChanged("WeebitType");
        if (temp.WeebitColor != weebitData.WeebitColor)
            this.OnPropertyChanged("WeebitColor");

        returnVal = true;
    }
    else
    {
        weebitData = temp;
        MessageBox.Show("Failed retrieveing data for:" + weebitData.Name);
    }

    isEnabled = false;
    this.OnPropertyChanged("IsItemEnabled");
    isNameEnabled = true;
    this.OnPropertyChanged("IsNameEnabled");

    return returnVal;
}
#endregion

The remaining functions are there to support the editing operations. And, they'll make more sense once we see how they are used. But, you can readily tell what they are intended to do, and you can also see that each simply relegates responsibility to somebody else. The Cancel method is used to re-load the data since it might have become dirty during editing. And, if data is re-loaded, each property is checked for changes, simply to minimize the events that will be generated.

Note that nothing so far has anything specifically related to the listbox. The WeebitData class, the WeebitService, and the WeebitPresentationModel could be used in a totally different environment (that's a good thing). Now, let's see about that editable listbox.

Selecting an Item

One of the problems we saw above is that the user control being used for the rendering essentially hides the listbox because it receives the attention. There are mechanisms for the listbox to detect user action using the different routing mechanisms available. But, I don't think it will produce a simpler solution. The approach presented here makes use of a combination of easily obtainable information. First, we define a trigger for the IsMouseOver property and use it to set the listbox's IsSelected property. This gives us some control over the selection and the user's actions.

XML
<Setter Property="Template">
    <Setter.Value>
    ...
        <ControlTemplate TargetType="{x:Type ListBoxItem}">
            <ControlTemplate.Triggers>
                <Trigger  Property="IsMouseOver" Value="True">
                    <Setter Property="IsSelected" Value="True"/>
                    ...
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Setter.Value>
</Setter>

Next, we want to provide some feedback to the user indicating which item 'would' be selected (like the focus feedback) as well as some kind of 'selector' mechanism. This could be a checkbox with some focus feedback, or a radio button. I didn't choose either of those because I wanted something that would be visible only when valid. Or perhaps, I just wanted the road less traveled. In any case, what I wanted was an arrow indicator that would point to the listboxitem and would move as the mouse moved over the listbox. The arrow would also be the selector through which the user would select an item for editing. And, once an item is selected for editing, I do not want the arrow to be displayed on any other item except the one being edited. So, where can we put this selector that is part of the listbox but outside of the user control?

You can define a template for the ListBoxItem to have any configuration as long as it has a ContentPresenter (because it's a ContentControl). We'll define a template for the ListBoxItem that will display a button on the left of the ContentPresenter which will contain our user control. And the selector button will be a blue arrow that will be invisible except when the mouse is over the listbox item. Here's the XAML for that, and Fig. 4 shows what it would look like:

XML
<Style x:Key="selectorButton" TargetType="{x:Type Button}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate>
                <Grid VerticalAlignment="Center" HorizontalAlignment="Center">
                    <Path Fill="Blue" Data="M 3,3 l 9,9 l -9,9 Z"></Path>
                </Grid>
            </ControlTemplate>

        </Setter.Value>
    </Setter>
</Style>

<Style TargetType="{x:Type ListBoxItem}">
...
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <Border x:Name="itemBorder" 
                          BorderThickness="2" 
                          BorderBrush="{Binding Converter={StaticResource 
                                       booleanToSolidColorBrushConverter}, 
                                       Path=IsItemEnabled}">
                    <DockPanel x:Name="listItem" Background="Transparent">
                        <Button x:Name="sideButton" 
                            Style="{StaticResource selectorButton}" 
                            Visibility="Hidden" 
                        Command="{Binding RelativeSource={RelativeSource FindAncestor, 
                                 AncestorType={x:Type ListBox}}, 
                                 Path=DataContext.SelectButtonCommand}">
                        </Button>
                        <ContentPresenter x:Name="contentPresenter">
                        </ContentPresenter>
                    </DockPanel>
                </Border>
                ...
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

ListboxitemWithSelector.PNG

Fig. 4 ListBox Item Selector

OK, almost there. So, as the user moves the cursor over the listbox items, the blue arrow will follow indicating which item will be selected. Then, if the user clicks on the blue arrow, we generate an event that will make the actual edit selection. But, there's one more point to cover before we go there. We still have all of the listbox items as editable. Which means that the user can select one with the arrow, and we can do whatever we want by hiding the arrow so s/he can't select any other one, but they can still move the cursor over to another item and start typing away. The answer is to make use of the IsHitTestVisible property for the ContentPresenter. That's the source for the IsItemEnable property that we saw exposed by the WeebitPresentationModel. It will be set to false by default, and will be set to true when the user selects an item. When the editing operation is completed, either with a save or a cancel, the property will be reset back to false. Now, the user can move the cursor all they want, and it won't bother us.

One more detail. We also defined a border above that will be displayed around the selected item. If an item is not selected for editing, the border will be transparent. This will give a good feedback to the user as to which item is currently selected for editing. The source for the border is the same property that sets IsHitTestVisible, IsItemEnable. So, if IsItemEnable is true, then we show the border and allow editing. A Converter translates the boolean value to a brush. Here's the XAML which is the key for the editable listbox:

XML
<ControlTemplate.Triggers>
    <Trigger  Property="IsMouseOver" Value="True">
        <Setter Property="IsSelected" Value="True"/>
        <Setter Property="Visibility"
            TargetName="sideButton"
            Value="{Binding RelativeSource={RelativeSource FindAncestor, 
                   AncestorType={x:Type ListBox}}, 
                   Path=DataContext.SelectorVisibility }"/>
        <Setter Property="IsHitTestVisible" 
            Value="{Binding Path=IsItemEnabled }"
            TargetName="contentPresenter"/>
    </Trigger>
</ControlTemplate.Triggers>

There's a little bit of magic that the listbox is doing for us. For each item in the listbox, we need an instance of the user control. The listbox handles that creation. Then, there needs to be a contextual relationship set up between the listboxitem View and the listboxitem data (source). The listbox is doing that. So, the above binding is done magically relative to the listboxitem. The visibility of the selector button, however, needs to be determined from a higher viewpoint. The EditorPresentationModel is the one that knows if there is any item selected for editing or not, which is part of what's needed to determine the visibility. So, to find the source of the Visibility value, we need to travel up the visual tree and find the listbox. Then, we can borrow the DataContext to get to the EditorPresentationModel. That's the binding that's defined above for the Visibility. There's nothing else of interest in the WeebitEditor View, so here's what it looks like, already cooked, and performing an edit operation. Now, let's continue with the EditorPresentationModel so we can wrap this up.

EditingAndValidation.PNG

Fig. 5 An Editable ListBox

The Home Stretch

As with the WeebitPresentationModel, the EditorPresentationModel is the enabler for the Editor View. So, it has to at least provide the endpoints for the bindings defined in the View. And likewise, in order for it to synchronize with the UI, it has to implement INotifyPropertyChanged.

C#
public class EditorPresentationModel : INotifyPropertyChanged
{
    enum ViewStates
    {
        NoSelect,
        EditingExisting,
        EditingNew
    }
    ViewStates currentState = ViewStates.NoSelect;

    ObservableCollection<WeebitPresentationModel> weebitListItems;

    IWeebitService weebitService;

    readonly DelegateCommand<string> newButtonCommand;
    readonly DelegateCommand<string> saveButtonCommand;
    readonly DelegateCommand<string> cancelButtonCommand;
    readonly DelegateCommand<string> selectButtonCommand;

    WeebitPresentationModel focusWeebit = null;
    WeebitPresentationModel editWeebit = null;

    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    public EditorPresentationModel(IWeebitEditor view, IWeebitService weebitService)
    {
        this.View = view;
        this.View.SetModel(this);
        this.weebitService = weebitService;

        weebitListItems = new ObservableCollection<WeebitPresentationModel>();
        
        foreach (WeebitData wd in weebitService.GetWeebits())
        {
            weebitListItems.Add(new WeebitPresentationModel(wd,weebitService));
        }

        newButtonCommand = new DelegateCommand<string>(OnNew, CanNew);
        saveButtonCommand = new DelegateCommand<string>(OnSave, CanSave);
        cancelButtonCommand = new DelegateCommand<string>(OnCancel, CanCancel);
        selectButtonCommand = new DelegateCommand<string>(OnSelect, CanSelect);
    }
    ...

This is the second piece of functionality that I'm utilizing from CAL. As you can see, the constructor for EditorPresentationModel requires two references to be passed in: one for the associated View which it is supporting, and the other is the repository service. The CAL container determines those dependencies, and takes care of the instantiation so it can pass in the appropriate references. As I mentioned previously, CAL in not mandatory to implement an editable listbox. The connection between the View and the PresentationModel can be made in other ways (see References below). And as far as the repository service, the EditorPresentationModel could easily create an instance of the repository service as a class member. The power of dependency injection, though, really comes through when there are multiple dependencies on the same service. For example, suppose there are two editors that need to have access to the WeebitService. Creating a local instance would not work in that case.

So, the first thing that gets done in the constructor is to set the DataContext for the View so that the bindings can take place. Next, we need to provide the listbox with a list of items, so we make use of the repository service to get the data, and we initialize an ObservableCollection of WeebitPresentationModels. The rest of the constructor deals with initializing the DelegateCommands for each of the buttons.

I suppose the best way to proceed from here is to describe the operations. Recall from above that we set up a trigger so that when the mouse is over a listbox item, the item is set to 'selected' (as far as the listbox is concerned). We have also bound the listbox's SelectedItem property to the FocusWeebitItem property, which is shown below:

C#
public WeebitPresentationModel FocusWeebitItem
{
    get { return focusWeebit; }
    set
    {
        if (value != null && focusWeebit != value)
        {
            focusWeebit = value;
            this.OnPropertyChanged("SelectorVisibility");
        }
    }
}
public Visibility SelectorVisibility
{
    get
    {
        if (currentState == ViewStates.NoSelect)
            return Visibility.Visible;
        else
        {
            if (focusWeebit == editWeebit)
                return Visibility.Visible;
            else
                return Visibility.Hidden;
        }
    }
}

Each time the SelectedItem changes, we'll set the internal variable focusWeebit to the selected item. This allows us to keep track of what the user is doing. At the same time, we also fire the PropertyChanged event for the SelectorVisibility property. The SelectorVisibility property is what controls our blue arrow visibility. We don't want it to be visible if there is a current selection and the mouse is over a different listbox item.

I guess it's time to explain a couple of class variables that I have not described. I'm maintaining a currentState variable which keeps track of what state the editor is in. There're only three states: nothing selected, editing an existing item, or editing a new item. This facilitates controlling the available user options (UI state). As we just saw, there is a class variable to keep track of which item the user is 'focusing on'. So, when the user makes the selection by clicking on the blue arrow, we know which item s/he intended. At that point, we then assign another variable, editWeebit, to remember the selection. Here's the code for that:

C#
void OnSelect(string notused)
{
    editWeebit = focusWeebit;
    currentState = ViewStates.EditingExisting;
    editWeebit.IsItemEnabled = true;
    editWeebit.IsNameEnabled = false;
}
bool CanSelect(string notused)
{
    if (currentState == ViewStates.NoSelect)
        return true;
    else
        return false;
}

As we just mentioned, when the user clicks on the selector button, we assign the editWeebit variable to focusWeebit so we can remember the selection. And, since we know the user selected an existing item in the listbox, we set currentState to EditingExisting. We'll use this information later when saving so we know if it's an update or a create. Next are the two properties that control how the listboxitem will display itself. Remember that from this position (this class), the only thing that is known is which listbox item the mouse is over. And, that is dynamic. And, each listbox item controls its own rendering. We described the IsNameEnabled property previously. Since the user selected an existing item, we set the property to false so that the Name field will be disabled. And the IsItemEnabled is what controls the IsHitTestVisible (and the border) for the ContentPresenter. Here, we're setting it to true so that the user can make changes to the item.

That's essentially it. The only things left are the three buttons that support the Save, Cancel, and New operations.

C#
void OnNew(string notused)
{
    currentState = ViewStates.EditingNew;
    editWeebit = new WeebitPresentationModel(new WeebitData());
    editWeebit.IsItemEnabled = true;
    weebitListItems.Add(editWeebit);
}
bool CanNew(string notused)
{
    bool returnVal = false;
    //Only if we are not already editing...
    if (currentState == ViewStates.NoSelect)
        returnVal = true;
    return returnVal;
}
void OnSave(string notused)
{
    if (currentState == ViewStates.EditingNew)
        editWeebit.Save(true,weebitService);
    else
        editWeebit.Save(false,weebitService);
    currentState = ViewStates.NoSelect;
    editWeebit = null;
}
bool CanSave(string notused)
{
    bool returnVal = false;
    if (currentState == ViewStates.EditingExisting ||
        currentState == ViewStates.EditingNew)
        returnVal = editWeebit.IsValid();
    return returnVal;
}
void OnCancel(string notused)
{
    if (currentState == ViewStates.EditingNew)
        weebitListItems.Remove(editWeebit);
    else
        editWeebit.Cancel(weebitService);

    editWeebit = null;
    currentState = ViewStates.NoSelect;
}
bool CanCancel(string notused)
{
    bool returnVal = false;
    //If we're editing then we can cancel...
    if (currentState == ViewStates.EditingExisting ||
        currentState == ViewStates.EditingNew)
        returnVal = true;
    return returnVal;
}

The New button is only enabled when there is nothing selected for editing. When the user selects the New button, we first set currentState to EditingNew so we know how to save the edits. Then, we create a new WeebitPresentationModel, initializing it with a new WeebitData object. We set IsItemEnabled to true since the data needs to be filled in, and we add the new item to the ObservableCollection so the listbox will update itself. There is a little room for improvement here. When an item is added, it gets placed at the bottom of the list. I tried a couple of options to see if the listbox would automatically scroll the item to a visible position, but I didn't spend too much time on it. As it is, the user has to scroll to reveal the new item.

The Save button is enabled only when an editing operation is in process and all the data fields have valid data. The WeebitData object is the one that determines if all the data is valid, as you can see from the call to IsValid. If the user cancels while editing a new item, the item can be simply deleted. Otherwise, the WeebitData object needs to re-initialize its data, and that's the Cancel method we saw previously.

I've left out the Delete functionality, but that can be easily implemented. The main thing is to add that function to WeebitService. The View just needs another button, and EditorPresentationModel needs to process it similar to the Save operation. The item would then be simply removed from the ObservableCollection.

That's a Wrap

Well, I learned a few things, and I hope this was of some use to you as well. I think the separated presentation pattern provides a much cleaner approach than what we've done in the past, so I wanted to explore that in more detail. And, I also exposed a little bit of the Composite Application Library in this article. It does offer a lot more, and I'll be expounding on those in the future. If you find anything in the article that could be done better, or need additional information on the areas I left out, please send in a comment, or write an article so we can all benefit.

References

  • Jammer wrote an article which gave me some initial ideas.
  • Josh Smith has written several articles that describe alternate approaches to commanding and view injection to those used above. In this article, he provides a complete description. He also wrote an article that allowed to me to address the CommandDelegate issue.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionFills The Bill Pin
Hyland Computer Systems5-Dec-13 13:28
Hyland Computer Systems5-Dec-13 13:28 
GeneralMy vote of 5 Pin
HipsterZipster6-Feb-13 7:22
HipsterZipster6-Feb-13 7:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.