zoukankan      html  css  js  c++  java
  • 结合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截图

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

  • 相关阅读:
    Ubuntu配置sublime text 3的c编译环境
    ORA-01078错误举例:SID的大写和小写错误
    linux下多进程的文件拷贝与进程相关的一些基础知识
    ASM(四) 利用Method 组件动态注入方法逻辑
    基于Redis的三种分布式爬虫策略
    Go语言并发编程总结
    POJ2406 Power Strings 【KMP】
    nyoj 会场安排问题
    Server Tomcat v7.0 Server at localhost was unable to start within 45 seconds. If the server requires more time, try increasing the timeout in the server editor.
    Java的String、StringBuffer和StringBuilder的区别
  • 原文地址:https://www.cnblogs.com/ywei221/p/4570689.html
Copyright © 2011-2022 走看看