WPF 的可编辑列表框






4.83/5 (7投票s)
一个易于实现的 WPF 可编辑列表框。
引言
这里有一个显示和编辑数据的处理方法,您可能会发现它很有用。它不能取代久负盛名的“数据网格”,但我认为它更容易实现,并且可能更适合某些应用程序。由于此处理方法使用的控件是列表框,因此它更适合字段数量较少且数据集大小可控的情况。但也可以通过某种形式的分页过滤来扩展它,以支持更大的数据集。
我认为这种处理方法,凭借其分层设计,代表了关注点的分离。虽然一开始(对我而言)它并不直观,但最终结果并没有什么特别之处。因此,只需最少的更改即可实现。
先决条件说明
我在演示应用程序中使用了复合应用程序库 (CAL)。但是,它不是构建可编辑列表框的必需项。我使用该库是因为我目前正在评估它,所以这更多是为了方便而不是其他原因。任何实现分离表示模式的设计都需要两件事:一种命令实现,以及一种将视图连接到表示模型(Presentation Model)的机制。CAL 在其 CommandDelegate
类中提供了 ICommand
的实现。这有助于我们在 UI 和支持逻辑之间实现我们想要的松耦合。CAL 还包含一个依赖注入容器,我使用它通过视图注入的形式在视图和表示模型之间建立连接。这两种要求都可以通过其他方式实现,而不是使用该库。在文章末尾的“参考文献”部分,我已指出其他方法的来源。
可编辑列表框?
创建一个可编辑列表框非常简单。您只需创建一个用户控件作为项的渲染器,为 ListBoxItem
定义一个模板以将用户控件设置为其内容,然后将其作为 DataTemplate
放入列表框中。
<Control.Resources>
...
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Border BorderBrush="Black"
BorderThickness="1"
Margin="2">
<StackPanel Orientation="Horizontal">
<ctl:WeebitItemCtl/>
</StackPanel>
</Border>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
...
</Control.Resources>
...
<ListBox IsSynchronizedWithCurrentItem="True"
Name="listWeebits"
ItemsSource="{Binding Path=WeebitListItems}"
SelectedItem="{Binding Mode=TwoWay, Path=FocusWeebitItem}"
Grid.ColumnSpan="5">
</ListBox>
...
如果您这样做,您将得到一个可编辑的列表框,但它不太实用。用户可以编辑每个项,但您无法控制它。
所以,这里的挑战在于管理它,使其既有用又直观。换句话说,表现正常。
第一个障碍是限制用户可以编辑的内容。我一开始的做法是禁用所有项,只留下选中的那一项可编辑。这对我来说效果不太好,因为当一项处于禁用模式时,您会丢失很多信息。它的可读性变差,而且正如您所见,颜色信息丢失了,这可能是用户做出选择所需要的。当然,看到所有项都禁用看起来很奇怪。
与此相关的问题是选择一项。由于用户交互是与用户控件进行的,列表框无法管理选择。要“选择”列表框中的一项的唯一方法是围绕用户控件留下一个用户可以单击的区域。这不是一个好的机制。在上图 1 中,您可以看到用户选择了一项,但同时也能修改另一项。
所以,挑战在于:
- 我们需要以某种方式限制用户的交互,使其仅限于正在编辑的项。一次只能编辑一项。
- 我们需要提供某种直观的选择机制,以便用户可以选择一项进行编辑。
- 一旦选择了一项,我们就需要一些反馈来向用户指示当前“选中”进行编辑的项。
- 我们还希望为用户提供一些视觉提示,说明输入数据的有效性。
- 并且,我们希望在 XAML 中实现其中大部分功能。
就列表框而言,就这些。当然,我们需要额外的功能来允许用户保存更改或取消操作。但那是(列表框)外部的功能。
数据
让我们从定义演示项目中使用的开始。以下是我们要处理的数据对象的定义。只是包含一些属性类型变化的示例。
我们有一个 WeebitData
对象,它具有名称、描述、物理尺寸、颜色和类型作为属性。类定义是一个简单的属性接口。该类的唯一有趣之处在于它还支持属性(数据)的验证。通过实现 IDataErrorInfo
,它可以直接挂钩到 UI 并为用户输入提供验证。看到我付出的努力很少就能获得这么多东西,这很有趣。当然,此处演示的验证非常基本。这是该类在此部分实现中的一瞥。
...
#region IDataErrorInfo
string IDataErrorInfo.Error { get { return null; } }
string IDataErrorInfo.this[string propertyName]
{
get { return this.GetValidationError(propertyName); }
}
#endregion
#region validations
string GetValidationError(string propertyName)
{
string error = null;
switch (propertyName)
{
case "Name":
error = this.ValidateName();
break;
case "Description":
error = this.ValidateDescription();
break;
case "Length":
error = this.ValidateFloat(this.Length,"Length");
break;
case "Width":
error = this.ValidateFloat(this.Width,"Width");
break;
case "Height":
error = this.ValidateFloat(this.Height,"Height");
break;
}
return error;
}
string ValidateName()
{
if (String.IsNullOrEmpty(this.Name))
{
return "Enter Name";
}
return null;
}
string ValidateDescription()
{
if (String.IsNullOrEmpty(this.Description))
{
return "Enter Description";
}
return null;
}
string ValidateFloat(string value, string item)
{
if (String.IsNullOrEmpty(value))
{
return "Enter "+item;
}
try
{
float temp = float.Parse(value);
return null;
}
catch
{
return "Invalid entry.";
}
}
#endregion
很简单。属性名称用作索引器,在评估属性的有效性后,如果有效则返回 null
,否则返回错误描述。当查看 UI 时,我们将看到如何将其挂钩。WeebitService
类提供对存储库的访问,并具有您典型的访问方法。那里实际上没有什么特别有趣的事情,所以我们将继续(可下载的项目包含完整源代码)。
Weebit 项用户控件
用于渲染 Weebit 数据的用户控件在上面的图 2 中有所描绘,其 XAML 如下所示。它是一个相当简单的控件,由几个文本框、一个组合框和一个按钮组成。
<UserControl x:Class="EditableListboxApp.Modules.Weebit.Views.WeebitItemCtl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Converters="clr-namespace:EditableListboxApp.Infrastructure.
Converters;assembly=EditableListboxApp.Infrastructure"
Height="Auto" Width="Auto">
<UserControl.Resources>
<Converters:ColorToSolidColorBrushConverter
x:Key="colorToSolidColorBrushConverter" />
<Style x:Key="tbWithValidation" TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Grid x:Name="ctlWnd" Height="Auto" Width="Auto">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60"></ColumnDefinition>
<ColumnDefinition Width="100"></ColumnDefinition>
<ColumnDefinition Width="40"></ColumnDefinition>
<ColumnDefinition Width="40"></ColumnDefinition>
<ColumnDefinition Width="40"></ColumnDefinition>
<ColumnDefinition Width="80"></ColumnDefinition>
<ColumnDefinition Width="50"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Name="textName" IsEnabled="{Binding Path=IsNameEnabled}"
Style="{StaticResource tbWithValidation}"
Text="{Binding Path=Name, ValidatesOnDataErrors=True,
UpdateSourceTrigger=PropertyChanged}" />
<TextBox Name="textDescription"
Style="{StaticResource tbWithValidation}" Grid.Column="1"
Text="{Binding Path=Description, ValidatesOnDataErrors=True,
UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Column="2" Name="textLength"
Style="{StaticResource tbWithValidation}"
Text="{Binding Path=Length, ValidatesOnDataErrors=True,
UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Column="3" Name="textWidth"
Style="{StaticResource tbWithValidation}"
Text="{Binding Path=Width, ValidatesOnDataErrors=True,
UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Column="4" Name="textHeight"
Style="{StaticResource tbWithValidation}"
Text="{Binding Path=Height, ValidatesOnDataErrors=True,
UpdateSourceTrigger=PropertyChanged}"/>
<Button Grid.Column="6" Name="buttonColor"
Background="{Binding Converter={StaticResource
colorToSolidColorBrushConverter}, Path=WeebitColor}"
Command="{Binding Path=ColorButtonCommand}">
Color...
</Button>
<ComboBox Grid.Column="5"
ItemsSource="{Binding Path=WeebitTypes, Mode=OneTime}"
Name="comboType" SelectedItem="{Binding Path=GSWeebitType}" />
</Grid>
</UserControl>
上面有几项可能需要一些解释。首先,需要转换器。为 Weebit 存储的颜色信息是一种颜色,但要渲染按钮的背景,我们需要一个画笔。转换器提供了从值到视觉的必要“转换”(转换也可以在表示模型中实现,但我认为这样更清晰)。其次,‘tbWithValidation
’ 为 TextBox
定义了一个触发器,该触发器指示我们希望如何显示验证错误。当出现验证错误时,我们希望将验证错误显示为工具提示文本。第三,您可能会想知道为什么名称字段被单独禁用。名称是数据的关键字段。当用户编辑现有项时,我们绝对不希望他们更改该字段。但当用户创建新条目时,我们必须允许访问。因此,这里是验证可以扩展的领域。可以在输入名称时进行检查,如果名称不唯一,则可以为验证错误返回适当的错误消息。
上面的每个项都将 ValidatesOnDataErrors
设置为 ‘true
’ 以挂钩验证检查。而且,我们希望在每次 PropertyChanged
事件时进行评估(是的,我走了捷径,我没有验证类型或颜色,这仅在新条目时才是必需的)。最后,所有项都绑定到 WeebitPresentationModel
,我们将在下一节中介绍。
WeebitPresentationModel
WeebitPresentationModel
类是视图(用户控件)的实现者。它是视图中定义的绑定的源。因此,它至少必须提供数据绑定的终结点。而且,为了提供所有 UI 魔术,它必须实现 INotifyPropertyChanged
以通过发起 UI 更新来保持 UI 与数据的同步。对我来说,这很棒。这就像盲目通信,因为你不知道你在和谁说话。最后,为了提供数据验证,它需要实现 IDataErrorInfo
,即使在这种情况下,它也只是一个传递。
public class WeebitPresentationModel : INotifyPropertyChanged, IDataErrorInfo
{
WeebitData weebitData;
readonly ICommand colorButtonCommand;
bool isEnabled = false;
bool isNameEnabled = true;
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public WeebitPresentationModel(WeebitData weebitData)
{
if (weebitData == null)
throw new ArgumentNullException("WeebitData");
this.weebitData = weebitData;
colorButtonCommand = new DelegateCommand<string>(OnColor);
}
#region weebit properties
public string Name
{
set
{
weebitData.Name = value;
this.OnPropertyChanged("Name");
CommandManager.InvalidateRequerySuggested();
}
get { return weebitData.Name; }
}
该类通过 WeebitData
对象的引用进行初始化,以便支持视图。对于此应用程序,我认为这是最简单的方法,因为每个列表框项实际上都是一个具有其支持表示模型的视图。WeebitPresentationModel
将所有与数据相关的责任都委托给了 WeebitData
对象。它的关注点主要是支持视图的要求。在构造函数中,我们还初始化了一个 DelegateCommand
用于 Weebit 颜色按钮。如前所述,这是我从 CAL 利用的便利设施之一。DelegateCommand
接受两个参数,都是方法。一个是用于确定 UI 控件状态的方法。另一个是在执行命令时调用的方法。在我们看到的例子中,颜色按钮只需要一个参数,因为按钮始终是启用的。
所有属性的处理方式与上面显示的 Name
属性非常相似。数据通过 WeebitData
对象传递并从 WeebitData
对象检索。任何时候数据发生更改,通知都会通过 OnPropertyChanged
事件传播到 UI。此处使用 CommandManager
需要稍作解释。这实际上是 CAL 提供的 DelegateCommand
实现不支持的内容。这是我需要的东西。由于我们正在验证用户输入,因此我们需要能够检查和更改 UI(按钮)的状态,同时更改每个属性,基本上是每次击键。特别是,如果任何属性无效,我希望阻止保存数据。InvalidateRequerySuggested
调用正在告诉 CommandManager
使它正在监视的命令控件的状态无效。这将触发对方法(我们将在稍后描述)的调用,这些方法确定我们按钮的状态。是不是听起来更清楚了?总之,为了使其正常工作,我对 CommandDelegate
类进行了修改。如前所述,您不必使用 CAL,参考文献部分提供了 CommandDelegate
的替代方案。
#region UI
public bool IsItemEnabled
{
get { return isEnabled; }
set
{
isEnabled = value;
this.OnPropertyChanged("IsItemEnabled");
CommandManager.InvalidateRequerySuggested();
}
}
public bool IsNameEnabled
{
get { return isNameEnabled; }
set
{
isNameEnabled = value;
this.OnPropertyChanged("IsNameEnabled");
CommandManager.InvalidateRequerySuggested();
}
}
public ICommand ColorButtonCommand
{
get { return colorButtonCommand; }
}
public string[] WeebitTypes
{
get { return Enum.GetNames(typeof(WeebitType)); }
}
public void OnColor(string notused)
{
ColorDialog dlg = new ColorDialog();
if (dlg.ShowDialog() == DialogResult.OK)
{
weebitData.WeebitColor = dlg.Color;
this.OnPropertyChanged("WeebitColor");
}
}
#endregion
稍后在讨论列表框本身时,我将描述 IsItemEnabled
属性。目前,它只是一个可设置的属性,但它没有与用户控件的 IsEnabled
属性绑定。IsNameEnabled
我之前已简要描述过,它确实代表了 UI 状态。一旦我们看到它们如何使用,这两者都会变得更清晰。
WeebitTypes
属性由 ComboBox 用于获取用户的可用选项。并且,当用户选择颜色按钮时,会调用 OnColor
方法。未使用的参数也是使用 CommandDelegate
的结果。
#region operations
public bool IsValid()
{
//All validation is handled by data object...
return weebitData.IsValid();
}
public bool Save(bool isNew)
{
if (isNew)
return weebitService.NewWeebit(weebitData);
else
return weebitService.UpdateWeebit(weebitData);
}
/// <summary>
/// We'll just re-load the data
/// </summary>
public bool Cancel()
{
bool returnVal = false;
WeebitData temp = weebitData; //Save it just in case
weebitData = weebitService.GetWeebit(weebitData.Name);
if (weebitData != null)
{
if (temp.Description != weebitData.Description)
this.OnPropertyChanged("Description");
if (temp.Length != weebitData.Length)
this.OnPropertyChanged("Length");
if (temp.Width != weebitData.Width)
this.OnPropertyChanged("Width");
if (temp.Height != weebitData.Height)
this.OnPropertyChanged("Height");
if (temp.WeebitType != weebitData.WeebitType)
this.OnPropertyChanged("WeebitType");
if (temp.WeebitColor != weebitData.WeebitColor)
this.OnPropertyChanged("WeebitColor");
returnVal = true;
}
else
{
weebitData = temp;
MessageBox.Show("Failed retrieveing data for:" + weebitData.Name);
}
isEnabled = false;
this.OnPropertyChanged("IsItemEnabled");
isNameEnabled = true;
this.OnPropertyChanged("IsNameEnabled");
return returnVal;
}
#endregion
其余函数用于支持编辑操作。一旦我们看到它们如何使用,它们就会更有意义。但是,您可以轻易地看出它们的用途,并且还可以看到它们都只是将责任委托给其他人。Cancel
方法用于重新加载数据,因为它在编辑过程中可能会变脏。并且,如果重新加载数据,将检查每个属性是否更改,仅仅是为了尽量减少将要生成的事件。
请注意,到目前为止,没有什么与列表框特别相关。WeebitData
类、WeebitService
和 WeebitPresentationModel
可以在完全不同的环境中(这是好事)使用。现在,让我们来看看那个可编辑列表框。
选择一项
我们上面看到的其中一个问题是,用于渲染的用户控件基本上隐藏了列表框,因为它吸引了所有注意力。列表框有机制可以使用可用的各种路由机制检测用户操作。但是,我认为这不会产生更简单的解决方案。此处提出的处理方法结合了易于获取的信息。首先,我们为 IsMouseOver
属性定义一个触发器,并使用它来设置列表框的 IsSelected
属性。这使我们能够控制选择和用户操作。
<Setter Property="Template">
<Setter.Value>
...
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="IsSelected" Value="True"/>
...
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
接下来,我们想为用户提供一些反馈,指示哪一项“将被”选中(类似于焦点反馈),以及某种“选择器”机制。这可能是一个带有焦点反馈的复选框,或者一个单选按钮。我没有选择其中任何一个,因为我想要一个仅在有效时才可见的东西。或者,也许我只是想走一条人迹罕至的路。总之,我想要一个箭头指示器,它指向列表框项,并且在鼠标移过列表框时移动。箭头也将是用户选择项进行编辑的选择器。一旦选择了某一项进行编辑,我不想在任何其他项上显示箭头,只在正在编辑的项上显示。那么,我们将这个属于列表框但又在用户控件之外的选择器放在哪里呢?
您可以为 ListBoxItem
定义一个模板,使其具有任何配置,只要它有一个 ContentPresenter
(因为它是一个 ContentControl
)。我们将为 ListBoxItem
定义一个模板,该模板将在 ContentPresenter
的左侧显示一个按钮,该按钮将包含我们的用户控件。选择器按钮将是一个蓝色箭头,除非鼠标悬停在列表框项上,否则它是不可见的。这是 XAML,图 4 显示了它的外观。
<Style x:Key="selectorButton" TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid VerticalAlignment="Center" HorizontalAlignment="Center">
<Path Fill="Blue" Data="M 3,3 l 9,9 l -9,9 Z"></Path>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type ListBoxItem}">
...
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border x:Name="itemBorder"
BorderThickness="2"
BorderBrush="{Binding Converter={StaticResource
booleanToSolidColorBrushConverter},
Path=IsItemEnabled}">
<DockPanel x:Name="listItem" Background="Transparent">
<Button x:Name="sideButton"
Style="{StaticResource selectorButton}"
Visibility="Hidden"
Command="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type ListBox}},
Path=DataContext.SelectButtonCommand}">
</Button>
<ContentPresenter x:Name="contentPresenter">
</ContentPresenter>
</DockPanel>
</Border>
...
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
好的,快完成了。因此,当用户将光标移到列表框项上时,蓝色箭头会跟随指示将选择哪一项。然后,如果用户单击蓝色箭头,我们会生成一个事件来实际选择编辑。但是,在继续之前,还有一点需要解决。我们仍然有所有列表框项都可编辑。这意味着用户可以用箭头选择一项,我们可以通过隐藏箭头来做任何我们想做的事情,这样他/她就不能选择任何其他项,但他们仍然可以将光标移到另一项并开始输入。答案是利用 ContentPresenter
的 IsHitTestVisible
属性。这是我们之前在 WeebitPresentationModel
中暴露的 IsItemEnable
属性的来源。它默认设置为 false
,当用户选择一项时会设置为 true
。当编辑操作完成(无论是保存还是取消)时,该属性将重置回 false
。现在,用户可以随意移动光标,这不会打扰我们。
再细节一点。我们上面还定义了一个边框,它将显示在选定项周围。如果未选择某一项进行编辑,边框将是透明的。这将为用户提供很好的反馈,说明当前选择的是哪一项进行编辑。边框的来源与设置 IsHitTestVisible
的属性相同,即 IsItemEnable
。因此,如果 IsItemEnable
为 true
,那么我们就显示边框并允许编辑。一个转换器将布尔值转换为画笔。这是关键的可编辑列表框的 XAML。
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="IsSelected" Value="True"/>
<Setter Property="Visibility"
TargetName="sideButton"
Value="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type ListBox}},
Path=DataContext.SelectorVisibility }"/>
<Setter Property="IsHitTestVisible"
Value="{Binding Path=IsItemEnabled }"
TargetName="contentPresenter"/>
</Trigger>
</ControlTemplate.Triggers>
列表框为我们做了一些神奇的事情。对于列表框中的每个项,我们需要一个用户控件实例。列表框处理该创建。然后,需要在列表框项视图和列表框项数据(源)之间建立上下文关系。列表框正在做这件事。因此,上面的绑定是相对于列表框项神奇地完成的。然而,选择器按钮的可见性需要从更高的视角来确定。EditorPresentationModel
是那个知道是否有任何项被选中进行编辑的类,这是确定可见性所需的一部分。所以,要找到 Visibility
值的来源,我们需要向上遍历视觉树并找到列表框。然后,我们可以借用 DataContext
来访问 EditorPresentationModel
。这就是上面为 Visibility
定义的绑定。WeebitEditor 视图中没有其他有趣的东西了,所以它看起来就是这样,已经准备就绪,并且正在执行编辑操作。现在,让我们继续介绍 EditorPresentationModel
,这样我们就可以完成这个了。
冲刺阶段
与 WeebitPresentationModel
一样,EditorPresentationModel
是 Editor 视图的实现者。因此,它至少必须提供视图中定义的绑定的终结点。同样,为了与 UI 同步,它必须实现 INotifyPropertyChanged
。
public class EditorPresentationModel : INotifyPropertyChanged
{
enum ViewStates
{
NoSelect,
EditingExisting,
EditingNew
}
ViewStates currentState = ViewStates.NoSelect;
ObservableCollection<WeebitPresentationModel> weebitListItems;
IWeebitService weebitService;
readonly DelegateCommand<string> newButtonCommand;
readonly DelegateCommand<string> saveButtonCommand;
readonly DelegateCommand<string> cancelButtonCommand;
readonly DelegateCommand<string> selectButtonCommand;
WeebitPresentationModel focusWeebit = null;
WeebitPresentationModel editWeebit = null;
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public EditorPresentationModel(IWeebitEditor view, IWeebitService weebitService)
{
this.View = view;
this.View.SetModel(this);
this.weebitService = weebitService;
weebitListItems = new ObservableCollection<WeebitPresentationModel>();
foreach (WeebitData wd in weebitService.GetWeebits())
{
weebitListItems.Add(new WeebitPresentationModel(wd,weebitService));
}
newButtonCommand = new DelegateCommand<string>(OnNew, CanNew);
saveButtonCommand = new DelegateCommand<string>(OnSave, CanSave);
cancelButtonCommand = new DelegateCommand<string>(OnCancel, CanCancel);
selectButtonCommand = new DelegateCommand<string>(OnSelect, CanSelect);
}
...
这是我从 CAL 利用的第二个功能。如您所见,EditorPresentationModel
的构造函数需要传递两个引用:一个用于它所支持的关联视图,另一个是存储库服务。CAL 容器确定这些依赖关系,并负责实例化,以便它可以传入适当的引用。如前所述,CAL 不是实现可编辑列表框的强制要求。视图和表示模型之间的连接可以通过其他方式实现(参见下面的参考文献)。至于存储库服务,EditorPresentationModel
可以很容易地创建一个存储库服务的实例作为类成员。然而,依赖注入的强大功能体现在当同一个服务有多个依赖项时。例如,假设有两个编辑器需要访问 WeebitService
。在这种情况下,创建本地实例将不起作用。
因此,构造函数中首先要做的就是为视图设置 DataContext
,以便可以进行绑定。接下来,我们需要为列表框提供一个项列表,因此我们使用存储库服务来获取数据,并初始化一个 ObservableCollection
的 WeebitPresentationModel
。构造函数的其余部分处理每个按钮的 DelegateCommand
的初始化。
我想从描述操作开始是最好的方法。回想一下,我们设置了一个触发器,以便当鼠标悬停在列表框项上时,该项被设置为“选中”(就列表框而言)。我们还已将列表框的 SelectedItem
属性绑定到 FocusWeebitItem
属性,如下所示。
public WeebitPresentationModel FocusWeebitItem
{
get { return focusWeebit; }
set
{
if (value != null && focusWeebit != value)
{
focusWeebit = value;
this.OnPropertyChanged("SelectorVisibility");
}
}
}
public Visibility SelectorVisibility
{
get
{
if (currentState == ViewStates.NoSelect)
return Visibility.Visible;
else
{
if (focusWeebit == editWeebit)
return Visibility.Visible;
else
return Visibility.Hidden;
}
}
}
每次 SelectedItem
更改时,我们都会将内部变量 focusWeebit
设置为选中的项。这使我们能够跟踪用户的操作。同时,我们还为 SelectorVisibility
属性触发 PropertyChanged
事件。SelectorVisibility
属性控制我们的蓝色箭头的可见性。如果我们当前有选中的项并且鼠标悬停在不同的列表框项上,我们不希望它可见。
我想是时候解释几个我还没有描述过的类变量了。我维护一个 currentState
变量,用于跟踪编辑器处于什么状态。只有三种状态:未选中任何项、正在编辑现有项或正在编辑新项。这有助于控制用户选项(UI 状态)。正如我们刚刚看到的,有一个类变量用于跟踪用户“关注”的项。因此,当用户通过单击蓝色箭头进行选择时,我们就知道他/她想要哪个项。此时,我们再将另一个变量 editWeebit
赋值给它,以记住选择。这是相关的代码。
void OnSelect(string notused)
{
editWeebit = focusWeebit;
currentState = ViewStates.EditingExisting;
editWeebit.IsItemEnabled = true;
editWeebit.IsNameEnabled = false;
}
bool CanSelect(string notused)
{
if (currentState == ViewStates.NoSelect)
return true;
else
return false;
}
正如我们刚才提到的,当用户单击选择器按钮时,我们将 editWeebit
变量赋值给 focusWeebit
,以便我们记住选择。并且,由于我们知道用户选择了一个列表框中的现有项,我们将 currentState
设置为 EditingExisting
。保存时,我们将使用此信息来知道是更新还是创建。接下来是控制列表框项如何显示的两个属性。请记住,从这个位置(这个类),唯一已知的是鼠标悬停在哪个列表框项上。而且,这是动态的。并且,每个列表框项都控制自己的渲染。我们之前描述了 IsNameEnabled
属性。由于用户选择了一个现有项,我们将该属性设置为 false
,以便 Name
字段将被禁用。而 IsItemEnabled
控制 ContentPresenter
的 IsHitTestVisible
(和边框)。在这里,我们将其设置为 true
,以便用户可以对该项进行更改。
基本上就是这些了。剩下的是支持保存、取消和新建操作的三个按钮。
void OnNew(string notused)
{
currentState = ViewStates.EditingNew;
editWeebit = new WeebitPresentationModel(new WeebitData());
editWeebit.IsItemEnabled = true;
weebitListItems.Add(editWeebit);
}
bool CanNew(string notused)
{
bool returnVal = false;
//Only if we are not already editing...
if (currentState == ViewStates.NoSelect)
returnVal = true;
return returnVal;
}
void OnSave(string notused)
{
if (currentState == ViewStates.EditingNew)
editWeebit.Save(true,weebitService);
else
editWeebit.Save(false,weebitService);
currentState = ViewStates.NoSelect;
editWeebit = null;
}
bool CanSave(string notused)
{
bool returnVal = false;
if (currentState == ViewStates.EditingExisting ||
currentState == ViewStates.EditingNew)
returnVal = editWeebit.IsValid();
return returnVal;
}
void OnCancel(string notused)
{
if (currentState == ViewStates.EditingNew)
weebitListItems.Remove(editWeebit);
else
editWeebit.Cancel(weebitService);
editWeebit = null;
currentState = ViewStates.NoSelect;
}
bool CanCancel(string notused)
{
bool returnVal = false;
//If we're editing then we can cancel...
if (currentState == ViewStates.EditingExisting ||
currentState == ViewStates.EditingNew)
returnVal = true;
return returnVal;
}
当没有项被选中进行编辑时,“新建”按钮才可用。当用户选择“新建”按钮时,我们首先将 currentState
设置为 EditingNew
,以便知道如何保存编辑。然后,我们创建一个新的 WeebitPresentationModel
,并用一个新的 WeebitData
对象进行初始化。我们将 IsItemEnabled
设置为 true
,因为需要填写数据,并将新项添加到 ObservableCollection
中,以便列表框自行更新。这里还有一些改进的空间。添加项时,它会添加到列表底部。我尝试了几种选项来看列表框是否会自动滚动到可见位置,但我没有花太多时间。目前,用户必须滚动才能显示新项。
当正在进行编辑操作且所有数据字段都有效时,“保存”按钮才可用。WeebitData
对象决定所有数据是否有效,正如您从对 IsValid
的调用中看到的。如果用户在编辑新项时取消,则可以简单地删除该项。否则,WeebitData
对象需要重新初始化其数据,这就是我们之前看到的 Cancel
方法。
我省略了删除功能,但这很容易实现。最主要的是将该功能添加到 WeebitService
。视图只需要另一个按钮,EditorPresentationModel
需要像处理保存操作一样处理它。然后该项将简单地从 ObservableCollection
中移除。
完结
好吧,我学到了一些东西,也希望这对您有所帮助。我认为分离表示模式提供了一种比我们过去使用的更清晰的处理方法,所以我希望更详细地探讨它。而且,我还在这篇文章中揭示了一些复合应用程序库的知识。它确实提供了更多功能,我将在未来对其进行详细阐述。如果您发现文章中有任何可以做得更好的地方,或者需要有关我遗漏的领域的更多信息,请发表评论或撰写文章,以便我们都能从中受益。