views:

45

answers:

3

Hi,

We are creating a business application (Silverlight 4) at our company where we, in several of our views, use the functionality to synchronize two lists of some kind.

More precise we are, at the moment, using two list boxes (one source and one target) and two buttons (one add and one remove). Both the source list and the target lists are bound to collections with the same data type.

A user may select one or more items in the source list and press the add button to have the items moved to the target list (i.e. the items are removed from the source and added to the target).

Likewise the user could select one or more items in the target list and press remove to have the items moved from the target back to the source list.

There is also the possibility to add a validation rule that says that the user must add at least one item to the target list.

Pretty simple...

Until now we are using our own created user control which incapsulates these 4 controls (2 list boxes and 2 buttons) and the logic for keeping the lists in sync. Dependency properties are used for binding the source and target collections.

Now to the problem. Our customer now wants our user control to be more flexible. They want to be able to have an arbitray number of columns in both the source and target list (i.e. the source list may have different columns and a different number of columns than the target list). The customer also wants to be able to sort on any column.

My first thought was to replace the list box to the data grid instead. But then I realized I don't know how to, in an easy way, let the consumer (the developer) define his or her columns and bindings. This may be to my limited knowledge of SL. Maybe a custom user control isn't the way to go?

I would appreciate any kind of help. Right now we are implementing the same logic over and over again in our views and it doesn't feel right. There has to be some way we can make this a reusable component that is easy to use.

Thanks!

A: 

"List version" of the answer

(see also the grid version)

A custom control sounds right for this, but you want both lists to be templated so the developer can define the per item views. I have assumed you do not need headings so have stuck with listboxes. The test code below results in this:

alt text

As the listboxes already have item templates, you really want to expose those as properties in your custom user control. You can then edit the 2 templates individually (the example below simply has both Left and Right templates set to the same FirstName/LastName stackpanel, this is where you define the format of your listboxes):

<UserControl
    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:SilverlightApplication1"
    mc:Ignorable="d"
    x:Class="SilverlightApplication1.TestListSelectionControl"
    d:DesignWidth="640" d:DesignHeight="480">
    <UserControl.Resources>
        <DataTemplate x:Key="DataTemplate1">
            <Grid>
                <StackPanel Orientation="Horizontal">
                    <TextBlock TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Top" Width="65" Text="{Binding FirstName}"/>
                    <TextBlock TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Top" Width="65" Text="{Binding LastName}"/>
                </StackPanel>
            </Grid>
        </DataTemplate>
    </UserControl.Resources>

    <Grid x:Name="LayoutRoot">
        <local:ListSelectionControl x:Name="SelectionControl" d:LayoutOverrides="Height" LeftItemTemplate="{StaticResource DataTemplate1}" RightItemTemplate="{StaticResource DataTemplate1}"/>
    </Grid>
</UserControl>

The example ListSelectionControl XAML is below:

<UserControl x:Class="SilverlightApplication1.ListSelectionControl"
    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"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <ListBox HorizontalAlignment="Stretch" Margin="12" Name="leftListBox" VerticalAlignment="Stretch" />
        <ListBox HorizontalAlignment="Stretch" Margin="12" Name="rightListBox" VerticalAlignment="Stretch" Grid.Column="2" />
        <StackPanel Grid.Column="1" Orientation="Vertical">
            <Button Content="Add &gt;" Height="23" HorizontalAlignment="Left" Width="75" Margin="10" />
            <Button Content="&lt; Remove" Height="23" HorizontalAlignment="Left" Width="75" Margin="10" />
        </StackPanel>
    </Grid>
</UserControl>

And the simple code-behind of the control:

using System.Windows;
using System.Windows.Controls;

    namespace SilverlightApplication1
    {
        public partial class ListSelectionControl : UserControl
        {
            public DataTemplate LeftItemTemplate
            {
                get
                {
                    return leftListBox.ItemTemplate;
                }

                set
                {
                    leftListBox.ItemTemplate = value;
                }
            }

            public DataTemplate RightItemTemplate
            {
                get
                {
                    return rightListBox.ItemTemplate;
                }

                set
                {
                    rightListBox.ItemTemplate = value;
                }
            }

            public ListSelectionControl()
            {
                InitializeComponent();

            }

        }
    }

And just to complete the example, this is the code behind to populate the sample GUI:

using System.Windows.Controls;
using System.Collections.ObjectModel;

namespace SilverlightApplication1
{
    public partial class TestListSelectionControl : UserControl
    {
        public TestListSelectionControl()
        {
            // Required to initialize variables
            InitializeComponent();
            SelectionControl.leftListBox.ItemsSource = Person.People;
            SelectionControl.rightListBox.ItemsSource = Person.Machines;
        }
    }

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public Person(string firstname, string lastname)
        {
            this.FirstName = firstname;
            this.LastName = lastname;
        }

        public static ObservableCollection<Person> People = new ObservableCollection<Person>()
                                                                {
                                                                    new Person("Tom", "Jones"),
                                                                    new Person("Elis", "Presley"),
                                                                    new Person("Joe", "Blogs")
                                                                };

        public static ObservableCollection<Person> Machines = new ObservableCollection<Person>()
                                                                {
                                                                    new Person("Marvin", "Android"),
                                                                    new Person("Hal", "9000"),
                                                                    new Person("B", "9")
                                                                };
    }
}
Enough already
Nice. Thanks!However our customer wants headings and the ability to sort each column. Is this possible to solve in an easy way with the listbox implementation or should one use the datagrid instead? And if so, how would that (datagrid) implementation differ from the above?
Wilco
I have added a second answer for grids as they use different techniques. Cheers
Enough already
+1  A: 

"Grid version" of the answer:

(see list version below)

As the question has changed (clarified) I am adding a new answer. The first one is still useful for those that only want lists so I will leave it there.

To do a similar thing with grids you don't expose the templates as datagrid columns are not templated (they are lists of controls, which can individually be templated). alt text

Instead you expose the left grid and right grid column collections as properties and simply set the LeftColumns and RightColumns properties of the control in your parent page. The only trick is that the datagrid-column collections have no setter so you need to update the grid collections items from the new property collections:

The code-behind is now:

using System.Collections.ObjectModel;
using System.Windows.Controls;

namespace SilverlightApplication1
{
    public partial class GridSelectionControl : UserControl
    {
        public GridSelectionControl()
        {
            InitializeComponent();
        }

        public ObservableCollection<DataGridColumn> LeftColumns
        {
            get
            {
                return leftDataGrid.Columns;
            }
            set
            {
                leftDataGrid.Columns.Clear();
                foreach (var col in value)
                {
                    leftDataGrid.Columns.Add(col);
                }
            }
        }

        public ObservableCollection<DataGridColumn> RightColumns
        {
            get
            {
                return rightDataGrid.Columns;
            }
            set
            {
                rightDataGrid.Columns.Clear();
                foreach (var col in value)
                {
                    rightDataGrid.Columns.Add(col);
                }
            }
        }
    }
}

The Xaml of the control is now:

<UserControl
    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:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" x:Class="SilverlightApplication1.GridSelectionControl"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <sdk:DataGrid x:Name="leftDataGrid" Margin="10" AutoGenerateColumns="False"/>
        <sdk:DataGrid x:Name="rightDataGrid" Margin="10" Grid.Column="2" AutoGenerateColumns="False"/>
        <StackPanel Grid.Column="1" Orientation="Vertical">
            <Button Content="Add &gt;" Height="23" HorizontalAlignment="Left" Width="75" Margin="10" />
            <Button Content="&lt; Remove" Height="23" HorizontalAlignment="Left" Width="75" Margin="10" />
        </StackPanel>

    </Grid>
</UserControl>

The Xaml of the test page (which defines the columns) is now:

<UserControl
    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:SilverlightApplication1" xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
    mc:Ignorable="d"
    x:Class="SilverlightApplication1.TestGridSelectionControl"
    d:DesignWidth="640" d:DesignHeight="480">

    <Grid x:Name="LayoutRoot">
        <local:GridSelectionControl x:Name="SelectionControl">
            <local:GridSelectionControl.LeftColumns>
                <sdk:DataGridCheckBoxColumn Header="Machine?" Binding="{Binding IsMachine}"/>
                <sdk:DataGridTextColumn Header="First Name" Binding="{Binding FirstName}"/>
                <sdk:DataGridTextColumn Header="Last Name" Binding="{Binding LastName}"/>
            </local:GridSelectionControl.LeftColumns>
            <local:GridSelectionControl.RightColumns>
                <sdk:DataGridCheckBoxColumn Header="Machine?" Binding="{Binding IsMachine}"/>
                <sdk:DataGridTextColumn Header="First Name" Binding="{Binding FirstName}"/>
                <sdk:DataGridTextColumn Header="Last Name" Binding="{Binding LastName}"/>
            </local:GridSelectionControl.RightColumns>
        </local:GridSelectionControl>
    </Grid>
</UserControl>

And the test code-behind is now:

using System.Collections.ObjectModel;
using System.Windows.Controls;

namespace SilverlightApplication1
{
    public partial class TestGridSelectionControl : UserControl
    {
        public TestGridSelectionControl()
        {
            // Required to initialize variables
            InitializeComponent();
            SelectionControl.leftDataGrid.ItemsSource = Person.People;
            SelectionControl.rightDataGrid.ItemsSource = Person.Machines;
        }

        public class Person
        {
            public string FirstName { get; set; }
            public string LastName { get; set; }
            public bool IsMachine { get; set; }

            public Person(string firstname, string lastname, bool robot)
            {
                this.FirstName = firstname;
                this.LastName = lastname;
                this.IsMachine = robot;
            }

            public static ObservableCollection<Person> People = new ObservableCollection<Person>()
                                                                {
                                                                    new Person("Tom", "Jones", false),
                                                                    new Person("Elis", "Presley", false),
                                                                    new Person("Joe", "Blogs", false)
                                                                };

            public static ObservableCollection<Person> Machines = new ObservableCollection<Person>()
                                                                {
                                                                    new Person("Marvin", "Android", true),
                                                                    new Person("Hal", "9000", true),
                                                                    new Person("B", "9", true)
                                                                };
        }
    }
}
Enough already
This is super! Thanks!!
Wilco
+1  A: 

Since you are creating a control to be consumed by other developers then its usually best to be using a Template Control rather than a UserControl. In which case the developers can specify a custom template for the control. However that's not as helpful as you could be, especially if the set of headers for both grids are the same.

One approach you could take is to provide dependency property of the type DataTemplate called "ListTemplate". At the two points in your controls Xaml where you would display the lists use two ControlPresenter elements. One named "SourceContent" the other "TargetContent". For both bind ContentTemplate to this new "ListTemplate".

Code up the assignment of the Content property on these presenters and then assign the appropriate collection the ItemsSource of either the ItemsControl or DataGrid the presenter has loaded.

If you include a simple ListBox base data template as the default value for the "ListTemplate" property then you control should be usuable in its simplest form yet if the developer wants use a DataGrid with various columns they can define one in the ListTemplate property.

Of course you will need to write code in your control to cope with the lists possibly being a DataGrid elements.

AnthonyWJones
+1: This brings up the point of *when to wear the extra expense of developing a full templated control vs. a simpler user control*. I agree with you that a templated approach has more flexibility, but this specific requirement (2 grids of varying columns) did not seem to justify the extra complexity of templating the list providers. I will however be creating one as you suggest, for future projects, to allow for both lists or grids (*or whatevers*) in one control. Cheers.
Enough already