使用MVVM模式管理多选功能

61

在学习MVVM的过程中,我已经初步理解了WPF和ViewModel模式。在提供列表并且关注单个被选中的项时,我使用以下抽象。

public ObservableCollection<OrderViewModel> Orders { get; private set; }
public ICollectionView OrdersView
{
    get
    {
        if( _ordersView == null )
            _ordersView = CollectionViewSource.GetDefaultView( Orders );
        return _ordersView;
    }
}
private ICollectionView _ordersView;

public OrderViewModel CurrentOrder 
{ 
    get { return OrdersView.CurrentItem as OrderViewModel; } 
    set { OrdersView.MoveCurrentTo( value ); } 
}
我可以将OrdersView与支持排序和过滤的内容绑定到WPF中的列表中:
<ListView ItemsSource="{Binding Path=OrdersView}" 
          IsSynchronizedWithCurrentItem="True">

这对于单选视图非常有效。但我想在该视图中还支持多项选择,并将模型绑定到所选项目列表。

我应该如何将ListView.SelectedItems绑定到ViewModel上的后备属性?

4个回答

95

在您的子ViewModel(在本例中为OrderViewModel)中添加一个 IsSelected 属性:

public bool IsSelected { get; set; }

将容器上的选定属性与此绑定(对于此情况中的 ListBox):

<ListBox.ItemContainerStyle>
    <Style TargetType="{x:Type ListBoxItem}">
        <Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}"/>
    </Style>
</ListBox.ItemContainerStyle>

IsSelected 被更新以匹配容器上相应的字段。

您可以通过以下方式在视图模型中获取所选的子元素:

public IEnumerable<OrderViewModel> SelectedOrders
{
    get { return Orders.Where(o => o.IsSelected); }
}

31
请注意,当在ListBox中使用VirtualizingStackPanel时(这是默认值),此解决方案不起作用。更多信息请参见此帖子:https://dev59.com/znM_5IYBdhLWcg3wrFMt - decasteljau
3
好的,谢谢你的更新。对于单选功能,最好的解决方案是使用 ICollectionView 接口。微软需要创建一个支持多选的 ICollectionView 接口。 - Josh G
5
如果您正在使用虚拟化面板,您将无法使用此方法。一个解决方法是在视图的代码后台中处理SelectionChanged事件。然后很容易在代码中将选择传递给VM……事件处理程序为您提供了所有新选择和取消选择的项目。 - Josh G
1
@JoshG 这对于从ListView绑定到视图模型是可以的,但它仍然没有解决从视图模型到ListView的绑定需求。如果一个当前滚动出视图的视图模型其IsSelected属性被设置为true,那么ListView将对此毫不知情。如果您有任何依赖于ListView的SelectedItems属性的代码,这将忽略已经通过编程方式设置了IsSelected的视图模型。我没有一个简单的解决方法,但只是想指出只处理SelectionChanged的限制。 - Mal Ross
@Mal Ross:使用虚拟化技术,这个问题很难解决。从我的角度来看,最好的解决方案是避免出现这个问题。如果你必须从代码中引用所选项,总是引用 VM 中的集合。这本来就是有意义的…VM 真正代表了视图的状态。ListView 应该只用于显示 VM 中包含的状态。使用所选项的代码应始终查询 VM 而不是视图。 - Josh G
我已经做过这么多次了,但每次都忘记我是怎么做的。谢谢! - Billy Jake O'Connor

12
我可以向您保证:SelectedItems 确实可以作为 XAML CommandParameter 进行绑定。
这个常见问题有一个简单的解决方案,只需按照以下全部规则进行操作:
  1. 按照Ed Ball的建议,在XAML命令数据绑定中,必须先定义CommandParameter属性,然后再定义Command属性。否则会出现一个非常耗时的错误。

    enter image description here

  2. 确保您的ICommandCanExecuteExecute方法具有object类型的参数。这样,您就可以防止数据绑定的CommandParameter类型与您的Command方法的参数类型不匹配时发生静默转换异常:

    private bool OnDeleteSelectedItemsCanExecute(object SelectedItems)  
    {
         // Your code goes here
    }
    
    private bool OnDeleteSelectedItemsExecute(object SelectedItems)  
    {
        // Your code goes here
    }
    

例如,您可以将ListView/ListBoxSelectedItems属性发送到ICommand方法或ListView/ListBox本身。很不错,不是吗?

我希望这可以防止有人像我一样花费大量时间来弄清楚如何将SelectedItems作为CanExecute参数接收。


2
我刚刚尝试了一下使用DataGrid,并以这种方式使用SelectedItems属性,我发现如果SelectedItems(即它没有反映出选择的当前状态),则CanExecute将使用先前的值进行调用。在其他控件中可能会有所不同,但在这种情况下被证明是无用的。 - Rob G
1
对我来说很好用(对于ListBox和DataGrid,使用.Net 4.0)。我将Command和CommandParameter的顺序颠倒了,但它仍然可以工作。 - BCA
2
很好,非常适合在4.5.2上使用ListView。需要补充的是,我还使用了Prism中的DelegateCommand来执行和判断是否可执行。所以:ICommand btnCommand = new DelegateCommand<object>(Execute,CanExecute); - CularBytes
难以置信!我尝试了很多次,有时它有效,有时它无效...这全都是关于顺序的问题... - Florian

3

可以尝试创建附加属性。

这样做可以避免为每个绑定的列表添加 IsSelected 属性。我已经在 ListBox 中完成了此操作,但它也可以修改为在列表视图中使用。

<ListBox SelectionMode="Multiple"
         local:ListBoxMultipleSelection.SelectedItems="{Binding SelectedItems}" >

更多信息:WPF - 绑定 ListBox 的 SelectedItems - 附加属性 vs 样式


1
我非常喜欢你的解决方案,但是我不得不修改你的代码,以便它不会在附加属性中缓存列表框。缓存列表框会阻止你在任何其他列表框上使用附加属性。 - Darkhydro

1

嗯,使用这种方法(视图>视图模型)实际上只能单向进行。无法像这里描述的那样进行视图模型>视图。 - Simon_Weaver
3
虽然这个回答从理论上回答了问题,但最好在这里包含回答的核心部分,并提供链接以供参考。 - Kev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接