结合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截图

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

时间: 2024-08-25 17:42:30

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

android 在布局中动态添加控件

第一步 Java代码 final LayoutInflater inflater = LayoutInflater.from(this); 第二步:获取需要被添加控件的布局 Java代码 final LinearLayout lin = (LinearLayout) findViewById(R.id.LinearLayout01); 第三步:获取需要添加的布局(控件) Java代码 LinearLayout layout = (LinearLayout) inflater.inflate( R

Android 在布局容器中动态添加控件

这里,通过一个小demo,就可以掌握在布局容器中动态添加控件,以动态添加Button控件为例,添加其他控件同样道理. 1.addView 添加控件到布局容器 2.removeView 在布局容器中删掉已有的控件 3.使用,来个小demo就明白了 public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(save

MFC中动态添加控件----寻找多年的秘籍,吐血推荐

原文作者tianwaik 动态控件是指在需要时由Create()创建的控件,这与预先在对话框中放置的控件是不同的. 一.创建动态控件 为了对照,我们先来看一下静态控件的创建. 放置静态控件时必须先建立一个容器(一般是对话框),这时我们在对话框编辑窗口中,从工具窗口中拖出所需控件放在对话框中即可,再适当修改控件ID,设置控件属性,一个静态控件就创建好了,当对话框被显示时,其上的控件也会显示. 相比之下,静态控件不需要调用Create()函数来创建. 而创建动态控件有很大不同,以下以按钮为例,看一下

New UI-Java代码动态添加控件或xml布局

New UI-Java代码动态添加控件或xml布局  --转载请注明出处:coder-pig,欢迎转载,请勿用于商业用途! 小猪Android开发交流群已建立,欢迎大家加入,无论是新手,菜鸟,大神都可以,小猪一个人的 力量毕竟是有限的,写出来的东西肯定会有很多纰漏不足,欢迎大家指出,集思广益,让小猪的博文 更加的详尽,帮到更多的人,O(∩_∩)O谢谢! 小猪Android开发交流群:小猪Android开发交流群群号:421858269 新Android UI实例大全目录:http://blog.

android 动态添加控件并实现每个子控件的点击事件

需求:我们要点击进入一家店铺,根据不同的店铺,显示不同条数的子条目 如:消毒间,洗菜间等...这些都是或多或少的,所以需要动态添加: 首先自定义View(linearLayout): package cn.qust.fang.widget; import io.vov.vitamio.MediaPlayer; import io.vov.vitamio.widget.MediaController; import io.vov.vitamio.widget.VideoView; import a

winform导入导出excel,后台动态添加控件

思路: 导入: 1,初始化一个OpenFileDialog类 (OpenFileDialog fileDialog = new OpenFileDialog();) 2, 获取用户选择文件的后缀名(string extension = Path.GetExtension(fileDialog.FileName).ToLower();),并设置允许后缀文件名: 3,NPOI转datetable,遍历tatetable转成实体类列表并入库: 导出: 1, 创建提示用户保存类,SaveFileDial

asp.net动态添加控件学习

看了老师的教程后,自己一点感悟记录下来: 1.在页面提交后,动态生成的控件会丢失, 但如果生成控件的代码在pageload中,就可以,原理是每次生成页面都执行生成. 2.动态按件或页面原来控件, 在页面往返重新生成时, 都有一个特点.就是控件里面的值和状态会保留下来. 如: 在DorpDownList中动态添加了 item项, 在页面往返后, 这个项是保留下来的, 选中值selected也是保留下来的. 在CheckBox中的值,或是动态添加的CheckBox中的值,页面返回后,其中的check

WPF 动态添加控件以及样式字典的引用(Style introduction)

原文:WPF 动态添加控件以及样式字典的引用(Style introduction) 我们想要达到的结果是,绑定多个Checkbox然后我们还可以获取它是否被选中,其实很简单,我们只要找到那几个关键的对象就可以了. 下面是Ui,其中定义了一个WrapPanel来存放CheckBox,还有两个按钮,用于测试相关功能. <Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.c

WPF 中动态改变控件模板

在某些项目中,可能需要动态的改变控件的模板,例如软件中可以选择不同的主题,在不同的主题下软件界面.控件的样式都会有所不同,这时即可通过改变控件模板的方式实现期望的功能. 基本方法是当用户点击切换主题按钮是加载新的资源字典,并使用新加载的资源字典替代当前的资源字典这时要用到ResourceManager. 假设现有两个不同的资源字典文件Dictionary1.xaml和Dictionary2.xaml存在于Themes文件夹内: 在MainPage中使用其中一个资源字典作为默认样式文件: <Win