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

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM