You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

WPF/MVVM:从ViewModel引用View及多选Adorner实现疑问

好问题!这确实是MVVM架构里处理可视化交互(比如Adorner这类依赖UI元素的操作)时常见的痛点,咱们一步步来拆解你的疑问:

你的当前方案是否违反MVVM原则?

首先得明确MVVM的核心是分离关注点:ViewModel应该只负责业务逻辑和状态管理,完全独立于View层的具体UI实现(比如WPF的FrameworkElement)。

你的方案里让IViewModel暴露Element属性,让ViewModel持有View的引用,这确实打破了这种独立性——现在你的ViewModel和WPF的UI控件强绑定了:不仅没法脱离UI环境做单元测试,也没法把这个ViewModel复用在其他UI框架里。所以这个方案确实违反了MVVM的核心原则。

替代实现方案(让ViewModel彻底和View解耦)

下面几个方案都围绕「ViewModel只维护选中状态,所有UI相关逻辑(比如Adorner的添加/移除)交给View层处理」的思路,完全符合MVVM要求:

方案1:利用ItemContainerGenerator获取选中项对应的控件

如果你的项是放在ItemsControl(比如ListBoxListView或者自定义的ItemsControl)里,可以通过容器的ItemContainerGenerator,根据ViewModel实例拿到对应的UI控件:

// 假设你有ItemsControl实例myItemsControl,以及选中的ViewModel集合selectedViewModels
foreach (var vm in selectedViewModels)
{
    var container = myItemsControl.ItemContainerGenerator.ContainerFromItem(vm) as FrameworkElement;
    if (container != null)
    {
        var adornerLayer = AdornerLayer.GetAdornerLayer(container);
        adornerLayer.Add(new YourCustomAdorner(container));
    }
}

这个方法的优势是ViewModel完全不用管View的事,所有UI逻辑都在View层(比如主窗口/UserControl的后台代码)处理。需要注意的是,ItemContainerGenerator只有在容器加载完成、对应项的UI已经生成后才能拿到控件,所以要确保在Loaded事件之后调用,或者监听ItemContainerGenerator.StatusChanged事件。

方案2:用附加属性绑定选中状态,自动处理Adorner

你可以创建一个附加属性,绑定到ViewModel的IsSelected属性,当状态变更时自动给目标控件添加/移除Adorner:

public static class AdornerHelper
{
    public static readonly DependencyProperty IsSelectedWithAdornerProperty =
        DependencyProperty.RegisterAttached(
            "IsSelectedWithAdorner",
            typeof(bool),
            typeof(AdornerHelper),
            new PropertyMetadata(false, OnIsSelectedChanged));

    public static bool GetIsSelectedWithAdorner(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsSelectedWithAdornerProperty);
    }

    public static void SetIsSelectedWithAdorner(DependencyObject obj, bool value)
    {
        obj.SetValue(IsSelectedWithAdornerProperty, value);
    }

    private static void OnIsSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is FrameworkElement element)
        {
            var adornerLayer = AdornerLayer.GetAdornerLayer(element);
            if (adornerLayer == null) return;

            if ((bool)e.NewValue)
            {
                // 添加自定义Adorner
                adornerLayer.Add(new YourCustomAdorner(element));
            }
            else
            {
                // 移除对应Adorner
                var adorners = adornerLayer.GetAdorners(element);
                if (adorners != null)
                {
                    foreach (var adorner in adorners.OfType<YourCustomAdorner>())
                    {
                        adornerLayer.Remove(adorner);
                    }
                }
            }
        }
    }
}

然后在XAML里给你的项控件绑定这个附加属性:

<UserControl x:Class="YourNamespace.MyItemControl"
             xmlns:local="clr-namespace:YourNamespace">
    <Border local:AdornerHelper.IsSelectedWithAdorner="{Binding IsSelected}">
        <!-- 你的项内容 -->
    </Border>
</UserControl>

这个方案完全通过XAML绑定实现,ViewModel只需要维护IsSelected属性,Adorner的逻辑封装在附加属性里,完全符合MVVM的分离原则。多选时,只要ViewModel的IsSelected被设为true,对应的UI控件会自动添加Adorner,根本不需要手动遍历获取控件。

方案3:使用Blend行为封装Adorner逻辑

如果你已经在用Blend的Behaviors库(可以通过NuGet安装Microsoft.Xaml.Behaviors.Wpf),可以创建一个行为来处理Adorner的添加/移除:

public class SelectedAdornerBehavior : Behavior<FrameworkElement>
{
    public static readonly DependencyProperty IsSelectedProperty =
        DependencyProperty.Register(nameof(IsSelected), typeof(bool), typeof(SelectedAdornerBehavior), new PropertyMetadata(false, OnIsSelectedChanged));

    public bool IsSelected
    {
        get => (bool)GetValue(IsSelectedProperty);
        set => SetValue(IsSelectedProperty, value);
    }

    private static void OnIsSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var behavior = d as SelectedAdornerBehavior;
        behavior?.UpdateAdorner();
    }

    private void UpdateAdorner()
    {
        var adornerLayer = AdornerLayer.GetAdornerLayer(AssociatedObject);
        if (adornerLayer == null) return;

        if (IsSelected)
        {
            adornerLayer.Add(new YourCustomAdorner(AssociatedObject));
        }
        else
        {
            var adorners = adornerLayer.GetAdorners(AssociatedObject);
            if (adorners != null)
            {
                foreach (var adorner in adorners.OfType<YourCustomAdorner>())
                {
                    adornerLayer.Remove(adorner);
                }
            }
        }
    }
}

然后在XAML里使用这个行为:

<UserControl x:Class="YourNamespace.MyItemControl"
             xmlns:i="http://schemas.microsoft.com/xaml/behaviors">
    <Border>
        <i:Interaction.Behaviors>
            <local:SelectedAdornerBehavior IsSelected="{Binding IsSelected}" />
        </i:Interaction.Behaviors>
        <!-- 你的项内容 -->
    </Border>
</UserControl>

这个方案和附加属性思路类似,但用行为的方式更符合WPF的组件化设计,逻辑封装更清晰,也更容易复用。

总结

你的原始方案确实违反了MVVM原则,因为ViewModel持有了View的引用。上面的几个方案都能让ViewModel只负责业务状态(选中状态),UI层自己处理Adorner这类可视化交互,完全符合MVVM的分离关注点要求。其中附加属性行为方案更推荐,因为它们可以通过XAML绑定实现,代码更简洁且易于维护。

内容的提问来源于stack exchange,提问作者mike

火山引擎 最新活动