结合ItemsControl在Canvas中动态添加控件的最MVVM的方式

今天很开心的收获: ItemsControl 中 ItemsPanel的重定义和 ItemContainerStyle 以及 ItemTemplate 三者的巧妙结合,在后台代码不实例化任何控件的前提下,实现标准的MVVM模式下,在前台Canvas中动态创建包含各种数据展示形态的控件。

好东西要共享,先上简化过的XAML最终解决方案:

 <UserControl.Resources>
        <Style x:Key="MyItemsControlStyle" TargetType="ItemsControl">
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <Canvas />
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="ItemContainerStyle">
                <Setter.Value>
                    <Style>
                        <Setter Property="Canvas.Left" Value="{Binding Left}" />
                        <Setter Property="Canvas.Top" Value="{Binding Top}" />
                    </Style>
                </Setter.Value>
            </Setter>
            <Setter Property="ItemTemplate">
                <Setter.Value>
                    <DataTemplate DataType="vm:MyItemViewModel">
                            <Border Width="120" Height="30" Background="Red">
                                <TextBlock Text="{Binding Name}" />
                            </Border>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>

    <Grid>
            <ItemsControl ItemsSource="{Binding ItemList}" Style="{StaticResource MyItemsControlStyle}" />
    </Grid>

 

看到这里大家可能不是很明白其中的有趣之处,那么下面是解决问题的整个过程。

说需求:

1. 需要根据业务数据,在界面的自定义位置显示数据对象。

2. 希望采用更符合MVVM设计模式的方式,界面和业务分离,在业务层添加数据的同时,界面自动创建数据对象对应的控件。

分析:这里面的自定义位置,需要绝对定位,那么自然要用到Canvas。

很久以前的做法是: 1. 创建一个自定义控件A

          2. 为自定义控件A扩展一堆自定义的属性。
          3. 每次新增业务对象时,在后台代码New一个自定义控件A的实例。

          4. Add到Canvas中,再按照业务数据,设置控件A的Canvas.Left和Canvas.Top。

这样的弊端是:如果业务数据频繁交互,那么Code-Behind中需要不停的引用界面中的控件,并使用代码维护和更新控件的各种属性。

以后一旦业务逻辑发生变更,后台代码中所有引用控件的地方都要跟着改动,类似过渡耦合导致的开发成本将会非常之高,最后变得不可维护。当然也有各种分层的方式可以很大程度上保持较高的扩展性和可维护性。但随着业务变化愈加复杂,随之而来的应对成本还是比较大的。想一想,还是有些不寒而栗。

我当然会继续使用界面和业务数据分离的方式来开发这个东西,但直到以我昨天对WPF的认知,想来想去也没有想明白该如何设置两个定位的值。

我起初尝试这样:

<UserControl.Resources>
        <Style x:Key="MyItemsControlStyle" TargetType="ItemsControl">
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <Canvas />
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="ItemTemplate">
                <Setter.Value>
                    <DataTemplate DataType="vm:MyItemViewModel">
                            <Border Canvas.Left="{Binding Left}" Canvas.Top="{Binding Top}" Width="120" Height="30" Background="Red">
                                <TextBlock Text="{Binding Name}" />
                            </Border>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>

 

必然不行,随后搜到了一位园友的文章。 http://www.cnblogs.com/fdyang/p/3877309.html

是个不错的方案,但有一点让我非常不舒服。就是在每个业务对象的数据模板中外面都包裹了一个Canvas,虽然这个Canvas是不可见的,不影响实际显示效果,但是如果我有一千个业务对象,界面就会创建一千个Canvas,而且所有的业务对象都不在同一个画布中,这无论如何不能忍···

 

随后在MSDN中发现了有人有比较类似的问题已经得到了解决

https://social.msdn.microsoft.com/Forums/vstudio/en-US/59a58867-352e-4c00-9ef2-5e2201ad18c6/bind-listbox-to-canvas-children?forum=wpf

MSDN里面的解决方案如下:

<ListBox x:Name="testListBox"  Width="300" Height="150"> 
            <ListBox.Template> 
                <ControlTemplate TargetType="{x:Type ListBox}"> 
                    <Canvas Background="Gray" x:Name="CanvasPanel" IsItemsHost="True" /> 
                </ControlTemplate> 
            </ListBox.Template > 
            <ListBox.ItemContainerStyle> 
                <Style TargetType="ListBoxItem"> 
                    <Setter Property="Canvas.Left" Value="{Binding (Canvas.Left)}"/> 
                     <Setter Property="Canvas.Top" Value="{Binding (Canvas.Top)}"/>    
                </Style> 
            </ListBox.ItemContainerStyle> 
            <ListBox.Items> 
                <Rectangle Width="50" Height="25" Canvas.Left="10" Canvas.Top="50" Fill="BlueViolet"/> 
                <Ellipse Width="50" Height="75" Canvas.Left="75" Canvas.Top="20" Fill="Blue"/> 
            </ListBox.Items> 
</ListBox> 

 

恍然大悟:哦,怎么没有想到呢。用ItemContainerStyle 进行Canvas附加属性的绑定就可以了啊。我以前都是使用ItemContainerStyle 绑定依赖属性,竟然忘记也可以绑定附加属性了。那么我和他的差别就是,他绑定的是控件自身的附加属性,而我的附加属性的值来源于ItemViewModel。最后使用 DataTemplete 设置 ItemTemplete 的数据可视化模板就可以了。

 

于是问题就这样解决了。为了确认这样是靠谱的,我用XamlPad查看了下 Visual Tree。

逻辑树如下:

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
<ItemsControl>
<ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <Canvas />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
    <Border Width="20" Canvas.Left="40" Canvas.Top="20" Height="30" Background="Red"></Border>
    <Border Width="20" Canvas.Left="80" Canvas.Top="40" Height="30" Background="Aqua"></Border>
</ItemsControl>
    </Grid>
</Page>

可视树截图:

 

好,那么现在我在ViewModel中,只需要创建一个 MyItemViewModel 的集合,叫做ItemList, 并绑定到 ItemsControl 的 ItemsSource 上,由于 DataTemplete 的 Type 是 MyItemViewModel,我只需要在后台代码中向集合添加 MyItemViewModel类型的实例,界面就创建了对应的控件,一共4行代码的方法。

        private void CreateMyItem()
        {
            ItemList.Add(new MyItemViewModel
            {
                Left = _rightButtonUpPoint.X,
                Top = _rightButtonUpPoint.Y,
                Name = string.Format("Left:{0} Top:{1}", _rightButtonUpPoint.X, _rightButtonUpPoint.Y)
            });
        }

最后上 Demo截图

 

本文原创,转载请注明出处。

posted @ 2015-06-12 12:40  蘑菇肉片  阅读(9470)  评论(9编辑  收藏  举报