Хорошо, благодаря Reflection и идее представления модели XCalibur, я могу выполнить свой пользовательский элемент управления, который может отображать содержимое двух общих наблюдаемых коллекций одного типа. Потребитель элемента управления может указать свойство списков объектов, которые должны отображаться. Данные коллекции будут скопированы в локальную коллекцию, и, следовательно, любые изменения, внесенные в коллекции, не изменят входные коллекции.
XAML:
<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:TimeTracker"
x:Class="TimeTracker.ItemsSelectionLists"
x:Name="ItemsSelectionControl">
<UserControl.Resources>
<local:DummyConverter x:Key="DummyConverter" />
</UserControl.Resources>
<Grid x:Name="LayoutRoot">
<Grid Background="#FFF9FDFD"
Margin="0,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Margin="8,8,0,0"
TextWrapping="Wrap"
Text="{Binding Path=LeftHeader, RelativeSource={RelativeSource AncestorType=UserControl}}"
VerticalAlignment="Top" />
<ListBox x:Name="LeftItemsList"
Grid.Column="0"
Margin="8,30,8,8"
MinWidth="150"
SelectionMode="Multiple"
ItemsSource="{Binding Path=LeftCollection}" />
<StackPanel Grid.Column="1"
Margin="0"
Orientation="Vertical"
VerticalAlignment="Center">
<Button Content=">"
Height="25"
Width="25"
Click="Button_Click" />
<Button Content=">>"
Height="25"
Width="25"
Click="Button_Click" />
<Button Content="<"
Height="25"
Width="25"
Click="Button_Click" />
<Button Content="<<"
Height="25"
Width="25"
Click="Button_Click" />
</StackPanel>
<TextBlock Grid.Column="2"
Margin="8,8,8,0"
TextWrapping="Wrap"
Text="{Binding Path=RightHeader, RelativeSource={RelativeSource AncestorType=UserControl}}"
VerticalAlignment="Top" />
<ListBox x:Name="RightItemsList"
Grid.Column="2"
Margin="8,30,8,8"
MinWidth="150"
SelectionMode="Multiple"
ItemsSource="{Binding Path=RightCollection}" />
</Grid>
</Grid>
</UserControl>
Код управления пользователем
public partial class ItemsSelectionLists : UserControl
{
#region properties
public string LeftHeader
{
get
{
return (string) GetValue(LeftHeaderProperty);
}
set
{
SetValue(LeftHeaderProperty, value);
}
}
// Using a DependencyProperty as the backing store for LeftHeader. This enables animation, styling, binding, etc...
public static readonly DependencyProperty LeftHeaderProperty =
DependencyProperty.Register("LeftHeader", typeof(string), typeof(ItemsSelectionLists), new UIPropertyMetadata("Left List Header"));
public string RightHeader
{
get
{
return (string) GetValue(RightHeaderProperty);
}
set
{
SetValue(RightHeaderProperty, value);
}
}
// Using a DependencyProperty as the backing store for RightHeader. This enables animation, styling, binding, etc...
public static readonly DependencyProperty RightHeaderProperty =
DependencyProperty.Register("RightHeader", typeof(string), typeof(ItemsSelectionLists), new UIPropertyMetadata("Right List Header"));
private object dataSource;
public object DataSource
{
get
{
return dataSource;
}
set
{
if (!value.GetType().FullName.StartsWith("TimeTracker.ViewModel"))
throw new ArgumentException("DataSource is not an instance of ViewModel");
if (dataSource != value)
{
dataSource = value;
this.DataContext = this.DataSource;
DataTemplateSelector templateSelector = dataSource as DataTemplateSelector;
this.LeftItemsList.ItemTemplateSelector = templateSelector;
this.RightItemsList.ItemTemplateSelector = templateSelector;
}
}
}
#endregion
public ItemsSelectionLists()
: base()
{
this.InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var button = sender as Button;
var type = dataSource.GetType();
var MoveItems = type.GetMethod("MoveItems");
var MoveAllItems = type.GetMethod("MoveAllItems");
switch (button.Content.ToString())
{
case ">":
MoveItems.Invoke(dataSource, new object[] { LeftItemsList, true });
break;
case ">>":
MoveAllItems.Invoke(dataSource, new object[] { true });
break;
case "<":
MoveItems.Invoke(dataSource, new object[] { RightItemsList, false });
break;
case "<<":
MoveAllItems.Invoke(dataSource, new object[] { false });
break;
}
}
}
ViewModel
public class ViewModel<T> : DataTemplateSelector, INotifyPropertyChanged
{
#region Properties
//this is just a placeholder for the collection, no changes will be made to this collection
private ObservableCollection<T> leftCollectionRef;
//local collection
private ObservableCollection<T> leftCollection;
public ObservableCollection<T> LeftCollection
{
get
{
return leftCollection;
}
set
{
if (value != leftCollectionRef)
{
//remove subscription to previous collection
if (leftCollectionRef != null)
leftCollectionRef.CollectionChanged -= new NotifyCollectionChangedEventHandler(Ref_CollectionChanged);
leftCollectionRef = value;
leftCollection.Clear();
foreach (var item in leftCollectionRef)
{
if (rightCollection.IndexOf(item) == -1)
leftCollection.Add(item);
}
NotifyPropertyChanged("LeftCollection");
//subscribe to chnages in new collection
leftCollectionRef.CollectionChanged += new NotifyCollectionChangedEventHandler(Ref_CollectionChanged);
}
}
}
//this is just a placeholder for the collection, no changes will be made to this collection
private ObservableCollection<T> rightCollectionRef;
private ObservableCollection<T> rightCollection;
public ObservableCollection<T> RightCollection
{
get
{
return rightCollection;
}
set
{
if (value != rightCollectionRef)
{
//remove subscription to previous collection
if (rightCollectionRef != null)
rightCollectionRef.CollectionChanged -= new NotifyCollectionChangedEventHandler(Ref_CollectionChanged);
rightCollectionRef = value;
rightCollection.Clear();
foreach (var item in rightCollectionRef)
{
if (leftCollection.IndexOf(item) == -1)
rightCollection.Add(item);
}
NotifyPropertyChanged("RightCollection");
rightCollectionRef.CollectionChanged += new NotifyCollectionChangedEventHandler(Ref_CollectionChanged);
}
}
}
private string bindingMember;
public string BindingMember
{
get
{
return bindingMember;
}
set
{
var mem = typeof(T).GetProperty(value);
if (mem == null)
throw new ArgumentException("No Member " + value + " found in " + this.GetType().FullName);
if (bindingMember != value)
{
bindingMember = value;
NotifyPropertyChanged("BindingMember");
}
}
}
#endregion
#region Constructors
public ViewModel()
: base()
{
// internal collection, this will get items copied over from reference source collection
leftCollection = new ObservableCollection<T>();
// internal collection, this will get items copied over from reference target collection
rightCollection = new ObservableCollection<T>();
bindingMember = "";
}
#endregion
#region Movements
public void MoveItems(ListBox list, bool LeftToRight)
{
var source = leftCollection;
var target = rightCollection;
if (!LeftToRight)
{
target = leftCollection;
source = rightCollection;
}
if (list.SelectedItems.Count > 0)
{
// List for items to be removed.
var hitList = new List<T>();
// Move items
foreach (T item in list.SelectedItems)
{
if (item != null)
{
// Tag item for removal
hitList.Add(item);
// Check if item is in target list
if (target.IndexOf(item) == -1)
{
target.Add(item);
}
}
}
// Remove items
foreach (var hitItem in hitList)
{
source.Remove(hitItem);
}
}
}
public void MoveAllItems(bool LeftToRight)
{
if (LeftToRight)
{
rightCollection.Clear();
foreach (var item in leftCollection)
{
RightCollection.Add(item);
}
leftCollection.Clear();
}
else
{
leftCollection.Clear();
foreach (var item in rightCollection)
{
leftCollection.Add(item);
}
rightCollection.Clear();
}
}
#endregion
#region collection-monitor
private void Ref_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null && e.NewItems.Count > 0)
{
var target = leftCollection;
if (sender == leftCollectionRef)
target = leftCollection;
else
target = rightCollection;
foreach (T item in e.NewItems)
{
target.Add(item);
}
}
//try remove from both collections, since the item may have moved to right or left collections
if (e.OldItems != null && e.OldItems.Count > 0)
{
foreach (T item in e.OldItems)
{
leftCollection.Remove(item);
}
foreach (T item in e.OldItems)
{
rightCollection.Remove(item);
}
}
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
#region templateselector
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
string dataTemplate =
@"<DataTemplate
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"">
<TextBlock Margin=""2"" TextWrapping=""Wrap"" Text=""{Binding Path=" + this.bindingMember + @", Mode=OneWay}""/>
</DataTemplate>";
StringReader stringReader = new StringReader(dataTemplate);
XmlReader xmlReader = XmlReader.Create(stringReader);
return XamlReader.Load(xmlReader) as DataTemplate;
}
#endregion
}
Использование элемента управления:
<TabItem Header="Resource Allocation">
<local:ItemsSelectionLists x:Name="ProjectResourceMap" LeftHeader="Whole Team" RightHeader="Current Project Team"/>
</TabItem>
<TabItem Header="Tasks for the Project">
<local:ItemsSelectionLists x:Name="ProjectTaskMap" Margin="0" d:LayoutOverrides="Width" LeftHeader="All Tasks" RightHeader="Current Project Tasks"/>
</TabItem>
ViewModel<Resource> ProjectResource = new ViewModel<Resource>();
ProjectResource.BindingMember = "ResourceName";
this.ProjectResourceMap.DataSource = ProjectResource;
ProjectResource.LeftCollection = timeTracker.Resources;
ViewModel<Task> ProjectTasks = new ViewModel<Task>();
ProjectTasks.BindingMember = "TaskName";
this.ProjectTaskMap.DataSource = ProjectTasks;
ProjectTasks.LeftCollection = timeTracker.Tasks;