tags:

views:

47

answers:

1

Hi all

I am trying to write a Dual List usercontrol in wpf.

I am new to wpf and I am finding it quite difficult. This is something I have put together in a couple of hours.It's not that good but a start.

I would be extremely grateful if somebody with wpf experience could improve it. The aim is to simplify the usage as much as possible

I am kind of stuck. I would like the user of the DualList Control to be able to set up headers how do you do that. Do I need to expose some dependency properties in my control?

At the moment when loading the user has to pass a ObservableCollection is there a better way?

Could you have a look and possibly make any suggestions with some code?

Thanks a lot!!!!!

xaml

   <Grid ShowGridLines="False">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="25px"></ColumnDefinition>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"></RowDefinition>
    </Grid.RowDefinitions>
    <StackPanel Orientation="Vertical" Grid.Column="0" Grid.Row="0">
        <Label Name="lblLeftTitle" Content="Available"></Label>
        <ListView Name="lvwLeft">
        </ListView>
    </StackPanel>
    <WrapPanel Grid.Column="1" Grid.Row="0">
        <Button Name="btnMoveRight" Content=">" Width="25" Margin="0,35,0,0" Click="btnMoveRight_Click" />
        <Button Name="btnMoveAllRight" Content=">>" Width="25" Margin="0,05,0,0" Click="btnMoveAllRight_Click" />
        <Button Name="btnMoveLeft" Content="&lt;" Width="25" Margin="0,25,0,0" Click="btnMoveLeft_Click" />
        <Button Name="btnMoveAllLeft" Content="&lt;&lt;" Width="25" Margin="0,05,0,0" Click="btnMoveAllLeft_Click" />
    </WrapPanel>
    <StackPanel Orientation="Vertical" Grid.Column="2" Grid.Row="0">
        <Label Name="lblRightTitle" Content="Selected"></Label>
        <ListView Name="lvwRight">
        </ListView>
    </StackPanel>
</Grid>

Client

       public partial class DualListTest
    {
        public ObservableCollection<ListViewItem> LeftList { get; set; }
        public ObservableCollection<ListViewItem> RightList { get; set; }

        public DualListTest()
        {
            InitializeComponent();
            LoadCustomers();
            LoadDualList();
        }

        private void LoadDualList()
        {
            dualList1.Load(LeftList, RightList);
        }

        private void LoadCustomers()
        {
            //Pretend we are getting a list of Customers from a repository.
            //Some go in the left List(Good Customers) some go in the Right List(Bad Customers).

            LeftList = new ObservableCollection<ListViewItem>();
            RightList = new ObservableCollection<ListViewItem>();

            var customers = GetCustomers();

            foreach (var customer in customers)
            {
                if (customer.Status == CustomerStatus.Good)
                {
                    LeftList.Add(new ListViewItem { Content = customer });
                }
                else
                {
                    RightList.Add(new ListViewItem{Content=customer });
                }
            }
        }

    private static IEnumerable<Customer> GetCustomers()
    {
        return new List<Customer>
                   {
                       new Customer {Name = "Jo Blogg", Status = CustomerStatus.Good},
                       new Customer {Name = "Rob Smith", Status = CustomerStatus.Good},
                       new Customer {Name = "Michel Platini", Status = CustomerStatus.Good},
                       new Customer {Name = "Roberto Baggio", Status = CustomerStatus.Good},
                       new Customer {Name = "Gio Surname", Status = CustomerStatus.Bad},
                       new Customer {Name = "Diego Maradona", Status = CustomerStatus.Bad}
                   };
    }
}

UserControl

         public partial class DualList:UserControl
        {
            public ObservableCollection<ListViewItem> LeftListCollection { get; set; }
            public ObservableCollection<ListViewItem> RightListCollection { get; set; }

            public DualList()
            {
                InitializeComponent();
            }

            public void Load(ObservableCollection<ListViewItem> leftListCollection, ObservableCollection<ListViewItem> rightListCollection)
            {
                LeftListCollection = leftListCollection;
                RightListCollection = rightListCollection;

                lvwLeft.ItemsSource = leftListCollection;
                lvwRight.ItemsSource = rightListCollection;

                EnableButtons();
            }
            public static DependencyProperty LeftTitleProperty = DependencyProperty.Register("LeftTitle",
                                                                                             typeof(string),
                                                                                             typeof(Label));

            public static DependencyProperty RightTitleProperty = DependencyProperty.Register("RightTitle",
                                                                                              typeof(string),
                                                                                              typeof(Label));


            public static DependencyProperty LeftListProperty = DependencyProperty.Register("LeftList",
                                                                                            typeof(ListView),
                                                                                            typeof(DualList));


            public static DependencyProperty RightListProperty = DependencyProperty.Register("RightList",
                                                                                            typeof(ListView),
                                                                                            typeof(DualList));
            public string LeftTitle
            {
                get { return (string)lblLeftTitle.Content; }
                set { lblLeftTitle.Content = value; }
            }
            public string RightTitle
            {
                get { return (string)lblRightTitle.Content; }
                set { lblRightTitle.Content = value; }
            }

            public ListView LeftList
            {
                get { return lvwLeft; }
                set { lvwLeft = value; }
            }
            public ListView RightList
            {
                get { return lvwRight; }
                set { lvwRight = value; }
            }

            private void EnableButtons()
            {
                if (lvwLeft.Items.Count > 0)
                {
                    btnMoveRight.IsEnabled = true;
                    btnMoveAllRight.IsEnabled = true;
                }
                else
                {
                    btnMoveRight.IsEnabled = false;
                    btnMoveAllRight.IsEnabled = false;
                }

                if (lvwRight.Items.Count > 0)
                {
                    btnMoveLeft.IsEnabled = true;
                    btnMoveAllLeft.IsEnabled = true;
                }
                else
                {
                    btnMoveLeft.IsEnabled = false;
                    btnMoveAllLeft.IsEnabled = false;
                }

                if (lvwLeft.Items.Count != 0 || lvwRight.Items.Count != 0) return;

                btnMoveLeft.IsEnabled = false;
                btnMoveAllLeft.IsEnabled = false;
                btnMoveRight.IsEnabled = false;
                btnMoveAllRight.IsEnabled = false;
            }

            private void MoveRight()
            {
                while (lvwLeft.SelectedItems.Count > 0)
                {
                    var selectedItem = (ListViewItem)lvwLeft.SelectedItem;
                    LeftListCollection.Remove(selectedItem);
                    RightListCollection.Add(selectedItem);
                }

                lvwRight.ItemsSource = RightListCollection;
                lvwLeft.ItemsSource = LeftListCollection;
                EnableButtons();
            }

            private void MoveAllRight()
            {
                while (lvwLeft.Items.Count > 0)
                {
                    var item = (ListViewItem)lvwLeft.Items[lvwLeft.Items.Count - 1];
                    LeftListCollection.Remove(item);
                    RightListCollection.Add(item);
                }

                lvwRight.ItemsSource = RightListCollection;
                lvwLeft.ItemsSource = LeftListCollection;
                EnableButtons();
            }

            private void MoveAllLeft()
            {
                while (lvwRight.Items.Count > 0)
                {
                    var item = (ListViewItem)lvwRight.Items[lvwRight.Items.Count - 1];
                    RightListCollection.Remove(item);
                    LeftListCollection.Add(item);
                }

                lvwRight.ItemsSource = RightListCollection;
                lvwLeft.ItemsSource = LeftListCollection;
                EnableButtons();
            }

            private void MoveLeft()
            {
                while (lvwRight.SelectedItems.Count > 0)
                {
                    var selectedCustomer = (ListViewItem)lvwRight.SelectedItem;
                    LeftListCollection.Add(selectedCustomer);
                    RightListCollection.Remove(selectedCustomer);
                }

                lvwRight.ItemsSource = RightListCollection;
                lvwLeft.ItemsSource = LeftListCollection;
                EnableButtons();
            }

            private void btnMoveLeft_Click(object sender, RoutedEventArgs e)
            {
                MoveLeft();
            }

            private void btnMoveAllLeft_Click(object sender, RoutedEventArgs e)
            {
                MoveAllLeft();
            }

            private void btnMoveRight_Click(object sender, RoutedEventArgs e)
            {
                MoveRight();
            }

            private void btnMoveAllRight_Click(object sender, RoutedEventArgs e)
            {
                MoveAllRight();
            }
        }
A: 

After looking over all of your code, it is clear to me that you are doing this the old WinForms way, rather than the WPF way. This isn't necessarily bad-- it still works, but it will be quite difficult to maintain. Rather than taking advantage of the many tools WPF gives us, such as data-binding, commands, templates, and dependency properties, you are pretty much wiring everything up with event handlers and writing a lot of code to maintain the UI. Using some of your original XAML and code, I have constructed a example that uses all of the aforementioned features. Rather than separate it into a UserControl, I have simply written it all in one Window for ease of demonstration. First, the XAML:

<Window x:Class="TestWpfApplication.DualList"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestWpfApplication"
Title="DualList" Height="300" Width="300"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
    <ObjectDataProvider x:Key="Customers" ObjectType="{x:Type local:Customer}" MethodName="GetCustomers"/>
    <CollectionViewSource x:Key="GoodCustomers" Source="{StaticResource Customers}" Filter="GoodFilter"/>
    <CollectionViewSource x:Key="BadCustomers" Source="{StaticResource Customers}" Filter="BadFilter"/>

    <DataTemplate DataType="{x:Type local:Customer}">
        <TextBlock Text="{Binding Name}"/>
    </DataTemplate>
</Window.Resources>
<Grid ShowGridLines="False">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="25"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <StackPanel Orientation="Vertical" Grid.Column="0" Grid.Row="0">
        <Label Name="lblLeftTitle" Content="{Binding LeftHeader, FallbackValue=Available}"/>
        <ListView Name="lvwLeft" MinHeight="200"
                  ItemsSource="{Binding Source={StaticResource GoodCustomers}}"/>
    </StackPanel>
    <WrapPanel Grid.Column="1" Grid.Row="0"
               DataContext="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}">
        <Button Name="btnMoveRight" Command="{Binding MoveRightCommand}" 
                CommandParameter="{Binding ElementName=lvwLeft, Path=SelectedItem}"
                Content="&gt;" Width="25" Margin="0,35,0,0"/>
        <Button Name="btnMoveAllRight" Command="{Binding MoveAllRightCommand}" 
                CommandParameter="{Binding Source={StaticResource GoodCustomers}}"
                Content="&gt;&gt;" Width="25" Margin="0,05,0,0"/>
        <Button Name="btnMoveLeft" Command="{Binding MoveLeftCommand}" 
                CommandParameter="{Binding ElementName=lvwRight, Path=SelectedItem}"
                Content="&lt;" Width="25" Margin="0,25,0,0"/>
        <Button Name="btnMoveAllLeft" Command="{Binding MoveAllLeftCommand}" 
                CommandParameter="{Binding Source={StaticResource BadCustomers}}"
                Content="&lt;&lt;" Width="25" Margin="0,05,0,0"/>
    </WrapPanel>
    <StackPanel Orientation="Vertical" Grid.Column="2" Grid.Row="0">
        <Label Name="lblRightTitle" Content="{Binding RightHeader, FallbackValue=Selected}"/>
        <ListView Name="lvwRight" MinHeight="200"
                  ItemsSource="{Binding Source={StaticResource BadCustomers}}"/>
    </StackPanel>
</Grid>

Starting from the top, the most important thing you will notice is that I am declaring an ObjectDataProvider and two CollectionViewSource objects. The data provider is bound to a method that will create your default customer list. The view sources take that list (of all customers) and filters them into two separate lists- one for good customers, and the other for bad customers. This is done through the CollectionViewSource.Filter property.

Next, you will see the user interface as you originally constructed it, but rather than wire up event handlers, I have bound the buttons to commands on the window. The ListView.ItemSource property is bound in the XAML, to the GoodCustomers and BadCustomers sources, respectively. All of these bindings will remove a good chunk of your boilerplate user-interface code. Now let's take a look at the code-behind:

public partial class DualList : Window
{
    public ICommand MoveRightCommand
    {
        get;
        set;
    }

    public ICommand MoveLeftCommand
    {
        get;
        set;
    }

    public ICommand MoveAllRightCommand
    {
        get;
        set;
    }

    public ICommand MoveAllLeftCommand
    {
        get;
        set;
    }

    public static DependencyProperty RightHeaderProperty =
        DependencyProperty.Register("RightHeader", typeof(string), typeof(DualList));

    public string RightHeader
    {
        get { return (string)GetValue(RightHeaderProperty); }
        set { SetValue(RightHeaderProperty, value); }
    }

    public static DependencyProperty LeftHeaderProperty =
        DependencyProperty.Register("LeftHeader", typeof(string), typeof(DualList));

    public string LeftHeader
    {
        get { return (string)GetValue(LeftHeaderProperty); }
        set { SetValue(LeftHeaderProperty, value); }
    }

    /// <summary>
    /// Default constructor-- set up RelayCommands.
    /// </summary>
    public DualList()
    {
        InitializeComponent();

        LeftHeader = "Good Customers";
        RightHeader = "Bad Customers";

        MoveRightCommand = new RelayCommand((o) => OnMoveRight((Customer)o), (o) => o != null);
        MoveLeftCommand = new RelayCommand((o) => OnMoveLeft((Customer)o), (o) => o != null);
        MoveAllRightCommand = new RelayCommand((o) => OnMoveAllRight((ListCollectionView)o), (o) => ((ListCollectionView)o).Count > 0);
        MoveAllLeftCommand = new RelayCommand((o) => OnMoveAllLeft((ListCollectionView)o), (o) => ((ListCollectionView)o).Count > 0);
    }

    /// <summary>
    /// Make this selected customer bad.
    /// </summary>
    private void OnMoveRight(Customer customer)
    {
        customer.Status = CustomerStatus.Bad;
        RefreshViews();
    }

    /// <summary>
    /// Make this selected customer good.
    /// </summary>
    private void OnMoveLeft(Customer customer)
    {
        customer.Status = CustomerStatus.Good;
        RefreshViews();
    }

    /// <summary>
    /// Make all customers bad.
    /// </summary>
    private void OnMoveAllRight(ListCollectionView customers)
    {
        foreach (Customer c in customers.SourceCollection)
            c.Status = CustomerStatus.Bad;
        RefreshViews();
    }

    /// <summary>
    /// Make all customers good.
    /// </summary>
    private void OnMoveAllLeft(ListCollectionView customers)
    {
        foreach (Customer c in customers.SourceCollection)
            c.Status = CustomerStatus.Good;
        RefreshViews();
    }

    /// <summary>
    /// Filters out any bad customers.
    /// </summary>
    private void GoodFilter(object sender, FilterEventArgs e)
    {
        Customer customer = e.Item as Customer;
        e.Accepted = customer.Status == CustomerStatus.Good;
    }

    /// <summary>
    /// Filters out any good customers.
    /// </summary>
    private void BadFilter(object sender, FilterEventArgs e)
    {
        Customer customer = e.Item as Customer;
        e.Accepted = customer.Status == CustomerStatus.Bad;
    }

    /// <summary>
    /// Refresh the collection view sources.
    /// </summary>
    private void RefreshViews()
    {
        foreach (object resource in Resources.Values)
        {
            CollectionViewSource cvs = resource as CollectionViewSource;
            if (cvs != null)
                cvs.View.Refresh();
        }
    }
}

Starting from the top, you will see an ICommand declaration for each button. I have also added two dependency properties that represent the right and left headers for your lists (changing these properties will automatically update the headers in the UI). Then, in the constructor, I hook up each command to a RelayCommand (created by Josh Smith), which simply lets me specify two delegates- one for when the command is executed, another that controls when the command can be executed. You can see that rather than moving items in between lists, I am simply changing the property on the item by which the list is filtered. So to move an item left, I change the customer status to good. This is an example of separation of UI and business logic: the UI reflects changes made to the underlying items, but does not make the changes itself.

The logic for each command should be fairly easy to understand- to move all customers to the good list, we simply iterate over each customer, setting their Status to good. Rinse and repeat for the other commands. Note that we must update our CollectionViewSource objects to make the filter update. Otherwise no changes would be shown.

So, in summary:

  • Don't write a bunch of code-behind to maintain your UI, use data-binding instead. CollectionViewSource and ObjectDataProvider can be used to filter and display your data the way you want it to. In your scenario, rather than manage two lists, I have a single list that gets filtered based on the customer status.
  • Don't use event handlers to set up UI logic, use commands. They increase encapsulation and allow for automatic updates to your UI.
  • Use dependency properties and binding to allow the user to customize your list headers.
Charlie
Thanks for you help.I was planning to use commands(DelegateCommand or RelayCommand) I was even thinking whether MVVM is feasible for userControls.But I wanted to get the basic working.Let me digest what you have done and come back to you.I can see some stuff that you have done that I dont understand.Again,I will be back to you asap.In the meantime THANKS!!!
HiBig thank you for your code.I have put it all in a new project and trying to get it running to get a feel but in the constructor the ListCollectionView needs initializing as it throws "Object variable not set",Never used ListCollectionView I guess is new to wpf.I agree 100% that I should avoid any wiring in the UI,just lack of knowledge really.I find xaml quite tricky with binding as there are many ways of doing things.Eventually it has to be a usercontrol or it will have no use.I need it for a real project.When I mention headers i Meant that each listview can bet set with columnHeaders
Hi, sorry but for some reasons I could not get more letters in my previous post.When moving to the userControl and moving items I would need to find a generic way to do things.The user of the control will decide which items to move.I have tried to use Generics but could not make it work with a userControl I dont think I can do "DualList<TItemType>.ListCollectionView seems pretty generic.Could it work the same as observableColllection<ListViewItem>?Any input on how I could maie it generic would be fantastic.Thanks a lot for your help
Concerning your error: make sure the CommandParameter bindings are set up correctly in the XAML. The CommandParameter is how the ListCollectionView is initialized. Concerning generics: right now you bind to a Customer class, which has string type data. But you can bind to any class with any type of data. You could create a generic class with a generic data type, and bind to that.
Charlie