MVVMを使用するWPFアプリケーションには、リストビューアイテムを持つユーザーコントロールがあります。実行時に、データバインディングを使用してリストビューをオブジェクトのコレクションで満たします。
リストビューのアイテムがダブルクリックされたときに、ビューモデルの対応するイベントが発生し、クリックされたアイテムへの参照を持つように、リストビューのアイテムにダブルクリックイベントを添付する正しい方法は何ですか?
クリーンなMVVMの方法、つまり、ビューにコードビハインドがない場合、どのように実行できますか?
コードビハインドは悪いことではありません。残念ながら、WPFコミュニティの多くの人々がこの間違いを犯しました。
MVVMは、コードビハインドを排除するパターンではありません。これは、ビュー部分(外観、アニメーションなど)をロジック部分(ワークフロー)から分離することです。さらに、ロジック部分の単体テストを行うことができます。
データバインディングはすべての解決策ではないため、コードビハインドを作成する必要がある十分なシナリオを知っています。あなたのシナリオでは、コードビハインドファイルでDoubleClickイベントを処理し、この呼び出しをViewModelに委任します。
コードビハインドを使用し、依然としてMVVM分離を実現するサンプルアプリケーションは、次の場所にあります。
WPFアプリケーションフレームワーク(WAF)- http://waf.codeplex.com
これを.NET 4.5で動作させることができます。簡単に思え、サードパーティやコードビハインドは必要ありません。
<ListView ItemsSource="{Binding Data}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<Grid Margin="2">
<Grid.InputBindings>
<MouseBinding Gesture="LeftDoubleClick" Command="{Binding ShowDetailCommand}"/>
</Grid.InputBindings>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Image Source="..\images\48.png" Width="48" Height="48"/>
<TextBlock Grid.Row="1" Text="{Binding Name}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Attached Command Behaviors およびCommandsを使用するのが好きです。 Marlon Grech には、Attached Command Behaviorsの非常に優れた実装があります。これらを使用して、ListViewの ItemContainerStyle プロパティにスタイルを割り当て、各ListViewItemにコマンドを設定できます。
ここでは、MouseDoubleClickイベントで発生するコマンドを設定し、CommandParameterはクリックするデータオブジェクトになります。ここでは、使用しているコマンドを取得するためにビジュアルツリーを上に移動していますが、アプリケーション全体のコマンドを簡単に作成できます。
<Style x:Key="Local_OpenEntityStyle"
TargetType="{x:Type ListViewItem}">
<Setter Property="acb:CommandBehavior.Event"
Value="MouseDoubleClick" />
<Setter Property="acb:CommandBehavior.Command"
Value="{Binding ElementName=uiEntityListDisplay, Path=DataContext.OpenEntityCommand}" />
<Setter Property="acb:CommandBehavior.CommandParameter"
Value="{Binding}" />
</Style>
コマンドについては、 ICommand を直接実装するか、 MVVM Toolkit に含まれるようなヘルパーを使用できます。
Blend SDK Eventトリガーを使用してこれを行う非常に簡単でクリーンな方法を見つけました。クリーンなMVVM、再利用可能、コードビハインドなし。
おそらくすでにこのようなものがあります:
<Style x:Key="MyListStyle" TargetType="{x:Type ListViewItem}">
まだ使用していない場合は、次のようなListViewItemのControlTemplateを含めます。
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListViewItem}">
<GridViewRowPresenter Content="{TemplateBinding Content}"
Columns="{TemplateBinding GridView.ColumnCollection}" />
</ControlTemplate>
</Setter.Value>
</Setter>
GridViewRowPresenterは、リスト行要素を構成する「内部」のすべての要素の視覚的なルートになります。ここでトリガーを挿入して、MouseDoubleClickルーティングイベントを探し、次のようにInvokeCommandActionを介してコマンドを呼び出すことができます。
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListViewItem}">
<GridViewRowPresenter Content="{TemplateBinding Content}"
Columns="{TemplateBinding GridView.ColumnCollection}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<i:InvokeCommandAction Command="{Binding DoubleClickCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</GridViewRowPresenter>
</ControlTemplate>
</Setter.Value>
</Setter>
GridRowPresenterの「上」に視覚的な要素がある場合(グリッドで始まる問題)、そこにトリガーを配置することもできます。
残念ながら、MouseDoubleClickイベントはすべての視覚要素から生成されるわけではありません(たとえば、FrameworkElementsからではなくControlsから生成されます)。回避策は、EventTriggerからクラスを派生させ、ClickCountが2のMouseButtonEventArgsを探すことです。これにより、MouseButtonEvents以外のすべてと、ClickCount!= 2のMoseButtonEventsを効果的に除外できます。
class DoubleClickEventTrigger : EventTrigger
{
protected override void OnEvent(EventArgs eventArgs)
{
var e = eventArgs as MouseButtonEventArgs;
if (e == null)
{
return;
}
if (e.ClickCount == 2)
{
base.OnEvent(eventArgs);
}
}
}
これを書くことができます(「h」は上のヘルパークラスの名前空間です):
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListViewItem}">
<GridViewRowPresenter Content="{TemplateBinding Content}"
Columns="{TemplateBinding GridView.ColumnCollection}">
<i:Interaction.Triggers>
<h:DoubleClickEventTrigger EventName="MouseDown">
<i:InvokeCommandAction Command="{Binding DoubleClickCommand}" />
</h:DoubleClickEventTrigger>
</i:Interaction.Triggers>
</GridViewRowPresenter>
</ControlTemplate>
</Setter.Value>
</Setter>
この議論は1年前のものであることに気付きましたが、.NET 4では、このソリューションについて何か考えはありますか? MVVMのポイントは、コードビハインドファイルを削除することではないことに絶対に同意します。また、何かが複雑だからといって、それが良いというわけではないと強く感じています。コードビハインドに入れたものは次のとおりです。
private void ButtonClick(object sender, RoutedEventArgs e)
{
dynamic viewModel = DataContext;
viewModel.ButtonClick(sender, e);
}
Caliburn のアクション機能を使用して、イベントをViewModelのメソッドにマッピングできます。 ItemActivated
にViewModel
メソッドがあると仮定すると、対応するXAMLは次のようになります。
<ListView x:Name="list"
Message.Attach="[Event MouseDoubleClick] = [Action ItemActivated(list.SelectedItem)]" >
詳細については、Caliburnのドキュメントとサンプルをご覧ください。
ビューが作成されたときにコマンドをリンクする方が簡単です:
var r = new MyView();
r.MouseDoubleClick += (s, ev) => ViewModel.MyCommand.Execute(null);
BindAndShow(r, ViewModel);
私の場合、BindAndShow
は次のようになります(updatecontrols + avalondock):
private void BindAndShow(DockableContent view, object viewModel)
{
view.DataContext = ForView.Wrap(viewModel);
view.ShowAsDocument(dockManager);
view.Focus();
}
ただし、このアプローチは、新しいビューを開く方法に関係なく機能します。
InuptBindingsでrushuiから解決策を見ましたが、テキストが存在しないListViewItemの領域にヒットすることができませんでした-背景を透明に設定した後でも、別のテンプレートを使用して解決しました。
このテンプレートは、ListViewItemが選択されアクティブになっている場合に使用します。
<ControlTemplate x:Key="SelectedActiveTemplate" TargetType="{x:Type ListViewItem}">
<Border Background="LightBlue" HorizontalAlignment="Stretch">
<!-- Bind the double click to a command in the parent view model -->
<Border.InputBindings>
<MouseBinding Gesture="LeftDoubleClick"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.ItemSelectedCommand}"
CommandParameter="{Binding}" />
</Border.InputBindings>
<TextBlock Text="{Binding TextToShow}" />
</Border>
</ControlTemplate>
このテンプレートは、ListViewItemが選択され、非アクティブになっている場合に使用します。
<ControlTemplate x:Key="SelectedInactiveTemplate" TargetType="{x:Type ListViewItem}">
<Border Background="Lavender" HorizontalAlignment="Stretch">
<TextBlock Text="{Binding TextToShow}" />
</Border>
</ControlTemplate>
これは、ListViewItemに使用されるデフォルトスタイルです。
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Border HorizontalAlignment="Stretch">
<TextBlock Text="{Binding TextToShow}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="True" />
<Condition Property="Selector.IsSelectionActive" Value="True" />
</MultiTrigger.Conditions>
<Setter Property="Template" Value="{StaticResource SelectedActiveTemplate}" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="True" />
<Condition Property="Selector.IsSelectionActive" Value="False" />
</MultiTrigger.Conditions>
<Setter Property="Template" Value="{StaticResource SelectedInactiveTemplate}" />
</MultiTrigger>
</Style.Triggers>
</Style>
気に入らないのは、TextBlockとそのテキストバインディングの繰り返しです。ただ1つの場所でそれを宣言できるかどうかわかりません。
これが誰かの助けになることを願っています!
ListBox
とListView
の両方で実行される動作を次に示します。
public class ItemDoubleClickBehavior : Behavior<ListBox>
{
#region Properties
MouseButtonEventHandler Handler;
#endregion
#region Methods
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.PreviewMouseDoubleClick += Handler = (s, e) =>
{
e.Handled = true;
if (!(e.OriginalSource is DependencyObject source)) return;
ListBoxItem sourceItem = source is ListBoxItem ? (ListBoxItem)source :
source.FindParent<ListBoxItem>();
if (sourceItem == null) return;
foreach (var binding in AssociatedObject.InputBindings.OfType<MouseBinding>())
{
if (binding.MouseAction != MouseAction.LeftDoubleClick) continue;
ICommand command = binding.Command;
object parameter = binding.CommandParameter;
if (command.CanExecute(parameter))
command.Execute(parameter);
}
};
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.PreviewMouseDoubleClick -= Handler;
}
#endregion
}
以下は、親を見つけるために使用される拡張クラスです。
public static class UIHelper
{
public static T FindParent<T>(this DependencyObject child, bool debug = false) where T : DependencyObject
{
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
//we've reached the end of the tree
if (parentObject == null) return null;
//check if the parent matches the type we're looking for
if (parentObject is T parent)
return parent;
else
return FindParent<T>(parentObject);
}
}
使用法:
xmlns:i="http://schemas.Microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.Microsoft.com/expression/2010/interactions"
xmlns:coreBehaviors="{{Your Behavior Namespace}}"
<ListView AllowDrop="True" ItemsSource="{Binding Data}">
<i:Interaction.Behaviors>
<coreBehaviors:ItemDoubleClickBehavior/>
</i:Interaction.Behaviors>
<ListBox.InputBindings>
<MouseBinding MouseAction="LeftDoubleClick" Command="{Binding YourCommand}"/>
</ListBox.InputBindings>
</ListView>