結合ItemsControl在Canvas中動態添加控件的最MVVM的方式

今天很開心的收穫: ItemsControl 中 ItemsPanel的重定義和 ItemContainerStyle 以及 ItemTemplate 三者的巧妙結合,在後臺代碼不實例化任何控件的前提下,實現標準的MVVM模式下,在前臺Canvas中動態建立包含各類數據展現形態的控件。html

好東西要共享,先上簡化過的XAML最終解決方案:canvas

 <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>

 

看到這裏你們可能不是很明白其中的有趣之處,那麼下面是解決問題的整個過程。設計模式

說需求:spa

1. 須要根據業務數據,在界面的自定義位置顯示數據對象。設計

2. 但願採用更符合MVVM設計模式的方式,界面和業務分離,在業務層添加數據的同時,界面自動建立數據對象對應的控件。code

分析:這裏面的自定義位置,須要絕對定位,那麼天然要用到Canvas。orm

好久之前的作法是: 1. 建立一個自定義控件Axml

          2. 爲自定義控件A擴展一堆自定義的屬性。
          3. 每次新增業務對象時,在後臺代碼New一個自定義控件A的實例。htm

          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截圖

 

本文原創,轉載請註明出處。

相關文章
相關標籤/搜索