Как реализовать CRUD операции при работе со связанными списками ListView(сценарий Master/Details) по шаблону MVVM?
У меня есть View(окно) оно поделено разделителем на 2 части:в левой размещены Master и справа Details ListView. К Master и Details привязаны ObservableCollection. Сверху этого View имеется панель с кнопками с привязанными к ним Command(Add, Delete, Edit,...). Я сделал для каждого ListView свою модель представления и связал их с соответствующими ListView, назовем их MasterVM и DetailsVM. В каждой из них находится свойство CurrentElement типа IBaseElement. Оба ListView с флагом IsSynchronizedWithCurrentItem=true. Имеются такие сущности:
public interface IBaseElement
{
/// Уникальный номер элемента.
int ID { get; set; }
}
/// Интерфейс для классов описывающих элементы данных раздела Category.
public interface ICategoryElement : IBaseElement
{
string Name { get; set; }
/// Коллекция дополнительных параметров элемента.
IList<IBaseElement> Details { get; set; }
}
/// Интерфейс для любых классов описывающих элемент данных образца.
public interface ISampleElement : ICategoryElement
{
/// Номер образца.
int Number { get; set; }
/// Диаметр образца.
float Diametr { get; set; }
/// Высота образца
float Height { get; set; }
/// Масса образца.
float Mass { get; set; }
/// Плотность образца.
float Density { get; set; }
}
/// Интерфейс для любых классов описывающих элемент данных стержня.
public interface IRodElement : ICategoryElement
{
/// Номер стержня.
int Number { get; set; }
/// Материал стержня.
string Material { get; set; }
}
/// Интерфейс для любых классов описывающих элемент данных прибора.
public interface IDeviceElement : ICategoryElement
{
/// Номер эталона.
int Number { get; set; }
/// Материал эталона.
string Material { get; set; }
}
/// Интерфейс для любых классов описывающих элемент данных эталона.
public interface IEtalonElement : ICategoryElement
{
/// Номер эталона.
int Number { get; set; }
/// Материал эталона.
string Material { get; set; }
}
/// Интерфейс для любых классов описывающих детали отдельно взятого образца.
public interface ISampleDetails : IBaseElement
{
/// Температура.
float Temperature { get; set; }
/// Теплоемкость.
float HeatCapacity { get; set; }
/// Коэффициент N0_Nt.
float N0_Nt { get; set; }
}
/// Интерфейс для любых классов описывающих детали отдельно взятого стержня.
public interface IRodDetails : IBaseElement
{
/// Температура.
float Temperature { get; set; }
/// Теплоемкость.
float HeatCapacity { get; set; }
}
/// Интерфейс для классов описывающих детали отдельно взятого устройства.
public interface IDeviceDetails : IBaseElement
{
/// Температура.
float Temperature { get; set; }
/// Коэффициент Kt.
float Kt { get; set; }
/// Коэффициент Pk.
float Pk { get; set; }
}
/// Интерфейс для любых классов описывающих детали отдельно взятого эталона.
public interface IEtalonDetails : IBaseElement
{
/// Температура.
float Temperature { get; set; }
/// Коэффициент N0_Nt.
float N0_Nt { get; set; }
/// Коэффициент SigmaC.
float SigmaC { get; set; }
/// Коэффициент Lambda_liter.
float Lambda_liter { get; set; }
/// Коэффициент Lambda.
float Lambda { get; set; }
/// Коэффициент Sigma_Percent.
float Sigma_Percent { get; set; }
}
Эти интерфейсы реализованы в соответствующих классах. Объекты типа ISampleElement, IRodElement, IDeviceElement, IEtalonElement это элементы коллекции Master, а с припиской Details это элементы Details. В левой части View есть ComboBox для выбора текущей группы с помощью такого перечисления:
/// Список категорий.
public enum Categories
{
/// Образцы
[Description("Образцы")]
Samples,
/// Стержень
[Description("Стержень")]
Rods,
/// Прибор
[Description("Прибор")]
Devices,
/// Эталон
[Description("Эталон")]
Etalons
}
Вид View задан в словаре ресурсов:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:wpf.test1.Framework.Converters"
xmlns:enums="clr-namespace:wpf.test1.Enums"
xmlns:helpers="clr-namespace:wpf.test1.Framework.Helpers"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
<!-- ============== КОНВЕРТЕРЫ ================= -->
<!-- Конвертер колонок для GridView -->
<converters:ConfigToDynamicGridViewConverter x:Key="ConfigToDynamicGridViewConverter" />
<!-- ============== СТИЛИ ================= -->
<!-- Стиль для элемента LV_CategoryItem из шаблона lv_CategoryDetailsTemplate -->
<Style x:Key="LV_Item" TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<!-- Стиль для элемента GridViewColumnHeader списка lv_CategoryDetails -->
<Style x:Key="LV_GridViewColumnHeader" TargetType="GridViewColumnHeader">
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<!-- Стиль LV_CategoryStyle -->
<Style x:Key="LV_CategoryStyle" TargetType="{x:Type ListView}">
<Setter Property="Margin" Value="2,5" />
<Setter Property="IsSynchronizedWithCurrentItem" Value="True" />
<Setter Property="View" Value="{Binding Category.ColumnsConfig, Converter={StaticResource ConfigToDynamicGridViewConverter}}" />
</Style>
<!-- Стиль LV_ElementDetailsStyle -->
<Style x:Key="LV_ElementDetailsStyle" TargetType="{x:Type ListView}">
<Setter Property="Margin" Value="2,5" />
<Setter Property="IsSynchronizedWithCurrentItem" Value="True" />
<Setter Property="View" Value="{Binding Details.ColumnsConfig, Converter={StaticResource ConfigToDynamicGridViewConverter}}" />
</Style>
<!-- ===================== ШАБЛОНЫ ================= -->
<!-- Шаблон данных для отдельного элемента ItemsControl -->
<DataTemplate x:Key="EditItemsTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="130" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Margin="2" Text="{Binding Text, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
<TextBox Grid.Column="1"
Margin="2"
Text="{Binding Value,
UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}" />
</Grid>
</DataTemplate>
<!-- Шаблон для пустого списка ItemsControl -->
<ControlTemplate x:Key="EmptyListTemplate">
<Border Margin="10"
BorderBrush="Black"
BorderThickness="1"
Padding="10">
<TextBlock>No items to display</TextBlock>
</Border>
</ControlTemplate>
<!-- Стиль для ItemsControl панели редактирования свойств элемента -->
<Style x:Key="EditItemsStyle" TargetType="{x:Type ItemsControl}">
<Setter Property="ItemTemplate" Value="{StaticResource EditItemsTemplate}" />
<Style.Triggers>
<DataTrigger Binding="{Binding ItemsSource, RelativeSource={RelativeSource Self}}" Value="{x:Null}">
<Setter Property="Template" Value="{StaticResource EmptyListTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding ItemsSource.Count, RelativeSource={RelativeSource Self}}" Value="0">
<Setter Property="Template" Value="{StaticResource EmptyListTemplate}" />
</DataTrigger>
</Style.Triggers>
</Style>
<!-- Шаблон данных для View режима просмотра -->
<DataTemplate x:Key="ViewModeTemplate">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Панель кнопок -->
<ToolBar Grid.Row="0">
<ComboBox Width="90"
DisplayMemberPath="Description"
ItemsSource="{Binding Source={helpers:EnumerationExtension {x:Type enums:Categories}}}"
SelectedValue="{Binding Category.CurrentCategory}"
SelectedValuePath="Value" />
<Button Content="Add" />
<Button Command="{Binding Delete}" Content="Delete" />
<Button Command="{Binding DeleteAll}" Content="Delete All" />
<Button Content="Update" />
<Button Content="Edit" />
<Button Command="{Binding FooCommand}" Content="Foo" />
</ToolBar>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Мастер(Список элементов выбранной категории) -->
<ListView x:Name="LV_Master"
Grid.Row="1"
Grid.Column="0"
ItemContainerStyle="{StaticResource LV_Item}"
ItemsSource="{Binding Path=Category.Items,
UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding Path=Category.CurrentElement,
UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}"
Style="{StaticResource LV_CategoryStyle}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="GotFocus">
<i:InvokeCommandAction Command="{Binding SetCurrentListCommand}" CommandParameter="{x:Static enums:MasterDetail.Master}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ListView>
<!-- Cписок с точками редактируемого элемента -->
<GridSplitter Grid.Row="1"
Grid.RowSpan="2"
Grid.Column="1"
Width="3"
HorizontalAlignment="Stretch" />
<ListView x:Name="LV_Details"
Grid.Row="1"
Grid.Column="2"
Grid.ColumnSpan="1"
ItemContainerStyle="{StaticResource LV_Item}"
ItemsSource="{Binding Path=Details.Items,
UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}"
SelectedItem="{Binding Path=Details.CurrentElement,
UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}"
Style="{StaticResource LV_ElementDetailsStyle}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="GotFocus">
<i:InvokeCommandAction Command="{Binding SetCurrentListCommand}" CommandParameter="{x:Static enums:MasterDetail.Detail}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ListView>
<!-- Разделитель -->
</Grid>
<!-- Строка состояния -->
<StatusBar Grid.Row="2">
<TextBlock Text="Статус:" />
</StatusBar>
</Grid>
</DataTemplate>
Это используется в окне так:
<Window
<ContentControl Content="{Binding}" ContentTemplate="{StaticResource ViewModeTemplate}" />
</Window>
Код конвертеров:
/// Класс конвертер для объекта GridView.
public class ConfigToDynamicGridViewConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var config = value as ColumnsConfig;
if (config != null)
{
var gridView = new GridView();
foreach (var column in config.Columns)
{
var binding = new Binding(column.DataField);
gridView.Columns.Add(new GridViewColumn { Header = column.Header, DisplayMemberBinding = binding });
}
return gridView;
}
return Binding.DoNothing;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
//Код расширителя разметки:
public class EnumerationExtension : MarkupExtension
{
private Type _enumType;
public EnumerationExtension(Type enumType)
{
if (enumType == null)
throw new ArgumentNullException("enumType");
EnumType = enumType;
}
public Type EnumType
{
get { return _enumType; }
private set
{
if (_enumType == value)
return;
var enumType = Nullable.GetUnderlyingType(value) ?? value;
if (enumType.IsEnum == false)
throw new ArgumentException("Type must be an Enum.");
_enumType = value;
}
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
var enumValues = Enum.GetValues(EnumType);
return (
from object enumValue in enumValues
select new EnumerationMember
{
Value = enumValue,
Description = GetDescription(enumValue)
}).ToArray();
}
private string GetDescription(object enumValue)
{
var descriptionAttribute = EnumType
.GetField(enumValue.ToString())
.GetCustomAttributes(typeof(DescriptionAttribute), false)
.FirstOrDefault() as DescriptionAttribute;
return descriptionAttribute != null
? descriptionAttribute.Description
: enumValue.ToString();
}
public class EnumerationMember
{
public string Description { get; set; }
public object Value { get; set; }
}
}
Идея такая: Пользователь выбирает в ComboBox нужную группу элементов(работает расширитель разметки для перечислений). View меняет колонки Master и Details ListView на соответствующие данным в коллекции Master(срабатывает конвертер). При выборе элемента в Master ListView должна сработать привязка для показа его подробностей в Details ListView. При этом данные для Details ListView должны браться из element.Details(поле Details это коллекция типа IList или что больше подойдет).
Вопросы: 1. Как настроить привязки для такой задачи? 2. Как реализовать добавление/удаление элементов в Master и Details ListView? Это те самые CRUD операции.
P.S. Не вижу кнопки для принятия ответа за "верный" могу только повышать очки ответов.