После просмотра всего вашего кода мне становится ясно, что вы делаете это старым WinForms способом, а не WPF способом. Это не обязательно плохо - все еще работает, но поддерживать его будет довольно сложно. Вместо того чтобы использовать преимущества множества инструментов, предоставляемых WPF, таких как привязка данных, команды, шаблоны и свойства зависимостей, вы в значительной степени связываете все с обработчиками событий и пишете много кода для поддержки пользовательского интерфейса. Используя некоторые из ваших исходных XAML и кода, я создал пример, который использует все вышеупомянутые функции. Вместо того, чтобы разделить его на UserControl
, я просто написал все это в одном Window
для простоты демонстрации. Во-первых, 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=">" Width="25" Margin="0,35,0,0"/>
<Button Name="btnMoveAllRight" Command="{Binding MoveAllRightCommand}"
CommandParameter="{Binding Source={StaticResource GoodCustomers}}"
Content=">>" Width="25" Margin="0,05,0,0"/>
<Button Name="btnMoveLeft" Command="{Binding MoveLeftCommand}"
CommandParameter="{Binding ElementName=lvwRight, Path=SelectedItem}"
Content="<" Width="25" Margin="0,25,0,0"/>
<Button Name="btnMoveAllLeft" Command="{Binding MoveAllLeftCommand}"
CommandParameter="{Binding Source={StaticResource BadCustomers}}"
Content="<<" 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>
Начиная сверху, самое важное, что вы заметите, это то, что я объявляю ObjectDataProvider
и два CollectionViewSource
объекта. Поставщик данных связан с методом, который создаст ваш список клиентов по умолчанию. Источники представлений берут этот список (всех клиентов) и фильтруют их в два отдельных списка: один для хороших клиентов, а другой для плохих клиентов. Это делается через свойство CollectionViewSource.Filter
.
Далее вы увидите пользовательский интерфейс в том виде, в котором он был изначально создан, но вместо того, чтобы подключать обработчики событий, я привязал кнопки к командам в окне. Свойство ListView.ItemSource
связано в XAML с источниками GoodCustomers
и BadCustomers
соответственно. Все эти привязки будут удалять большую часть кода вашего пользовательского интерфейса. Теперь давайте посмотрим на код:
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();
}
}
}
Начиная сверху, вы увидите объявление ICommand
для каждой кнопки. Я также добавил два свойства зависимостей, которые представляют правый и левый заголовки для ваших списков (изменение этих свойств автоматически обновит заголовки в пользовательском интерфейсе). Затем в конструкторе я подключаю каждую команду к RelayCommand
(созданному Джошем Смитом), который просто позволяет мне указать двух делегатов - один для выполнения команды, другой для управления, когда Команда может быть выполнена. Вы можете видеть, что вместо перемещения элементов между списками я просто изменяю свойство элемента, по которому фильтруется список. Поэтому, чтобы переместить товар влево, я изменяю статус клиента на хороший. Это пример разделения пользовательского интерфейса и бизнес-логики : пользовательский интерфейс отражает изменения, внесенные в базовые элементы, но не вносит сами изменения.
Логика каждой команды должна быть достаточно простой для понимания - чтобы переместить всех клиентов в список хороших, мы просто выполняем итерацию по каждому клиенту, устанавливая их Status
на хорошее. Промойте и повторите для других команд. Обратите внимание, что мы должны обновить наши CollectionViewSource
объекты, чтобы обновить фильтр. В противном случае изменения не будут отображаться.
Итак, в итоге:
- Не пишите много кода для поддержки вашего интерфейса, вместо этого используйте привязку данных.
CollectionViewSource
и ObjectDataProvider
могут быть использованы для фильтрации и отображения ваших данных так, как вы хотите. В вашем сценарии вместо управления двумя списками у меня есть один список, который фильтруется по статусу клиента.
- Не используйте обработчики событий для настройки логики пользовательского интерфейса, используйте команды. Они увеличивают инкапсуляцию и позволяют автоматически обновлять ваш пользовательский интерфейс.
- Используйте свойства зависимостей и привязку, чтобы пользователь мог настраивать заголовки списка.