简体   繁体   中英

WPF Treeview Not Updating After Rearchitecting to MVVM

I've created an application to load specifically-formated XML files and edit its fields through a WPF GUI. However, being new to C#, I was not aware of the MVVM pattern upon writing it, and I'd taken the quick and dirty route of simply coding everything in the code-behind. However, Upon rearchitecting, I cannot seem to get the treeview (that was working before) to update anymore.

Here's the relevant XAML:

<UserControl x:Class="TableBuilder.Views.XmlView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:views="clr-namespace:TableBuilder.Views"
             xmlns:viewmodels="clr-namespace:TableBuilder.ViewModels"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.DataContext>
        <viewmodels:XmlViewModel />
    </UserControl.DataContext>
    <DockPanel>
        <TreeView x:Name="XmlTree" SelectedItemChanged="XmlView_SelectedItemChanged">
            <TreeView.Resources>
                <Style TargetType="{x:Type TreeViewItem}">
                    <Setter Property="HeaderTemplate">
                        <Setter.Value>
                            <DataTemplate>
                                <StackPanel Orientation="Horizontal">
                                    <Image Width="20" Margin="2" Source="../Images/Red_Bullet.png" />
                                    <TextBlock VerticalAlignment="Center" Text="{Binding}" />
                                </StackPanel>
                            </DataTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </TreeView.Resources>
        </TreeView>
    </DockPanel>
</UserControl>

Here's the code-behind:

using System;
using System.Windows;
using System.Windows.Controls;
using TableBuilder.Helpers;

namespace TableBuilder.Views
{
    /// <summary>
    /// Interaction logic for XmlView.xaml
    /// </summary>
    public partial class XmlView : UserControl
    {
        RelayCommand selectedItemChanged { get; set; }
        public XmlView()
        {
            InitializeComponent();
        }

        private void XmlView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            //((MainViewModel)this.DataContext).SelectedItemChangedCommand = (RelayCommand)sender;
            Console.WriteLine("XmlView selected item changed event triggered");
        }
    }
}

Here's the View Model:

using System;
using System.Collections.Generic;
using System.Windows.Controls;
using System.Xml;
using TableBuilder.Helpers;
using TableBuilder.Models;

namespace TableBuilder.ViewModels
{
    class XmlViewModel : ViewModelBase
    {
        private TreeView _xmlTree;
        public TreeView XmlTree
        {
            get { return _xmlTree; }
            set
            {
                _xmlTree = value;
                OnPropertyChanged("XmlTree");
            }
        }

        public XmlViewModel()
        {
            xmlModel = new XmlModel();

            TreeView XmlTree = new TreeView();
            TreeViewItem node = new TreeViewItem();
            node.Header = "Test";
            XmlTree.Items.Add(node);

            xmlModel.PropertyChanged += XmlModel_CurrentChanged;
        }

        private void XmlModel_CurrentChanged(object sender, EventArgs e)
        {
            TreeViewItem xmlTreeItems = GetTreeViewItems();
            TreeView XmlTree = new TreeView();
            XmlTree.Items.Add(xmlTreeItems);

        }

        private TreeViewItem GetTreeViewItems()
        {
            // Find the filename of the XML document
            Uri uri = new Uri(xmlModel.XmlDoc.BaseURI);
            string filename = "";
            if (uri.IsFile)
            {
                filename = System.IO.Path.GetFileName(uri.LocalPath);
            }

            // Populate the TreeView
            TreeViewItem TitleNode = new TreeViewItem()
            {
                Header = filename,
                Tag = "Title Node",
            };
            PopulateTree(TitleNode, xmlModel.XmlDoc.ChildNodes);
            return TitleNode;
        }

        private void PopulateTree(TreeViewItem parent, XmlNodeList nodes)
        {
            TreeViewItem item;
            Dictionary<string, string> dict;
            bool isSmallestChild;
            foreach (XmlNode node in nodes)
            {

                // Ignore all comments in the XML document
                if (!node.Name.Contains("#comment"))
                {
                    // Add to TreeView
                    item = toTreeViewItem(node);

                    isSmallestChild = false;
                    dict = new Dictionary<string, string>();
                    foreach (XmlNode child in node.ChildNodes)
                    {
                        if (child.Name.Contains("type") ||
                            child.Name.Contains("value") ||
                            child.Name.Contains("units"))
                        {
                            isSmallestChild = true;
                        }
                        if (isSmallestChild)
                        {
                            dict.Add(child.Name, child.InnerText);
                        }
                    }
                    if (!isSmallestChild)
                    {
                        PopulateTree(item, node.ChildNodes);
                    }
                    else
                    {
                        item.Tag = dict;
                    }
                    parent.Items.Add(item);
                }
            }
        }

        private TreeViewItem toTreeViewItem(XmlNode node)
        {
            TreeViewItem item = new TreeViewItem()
            {
                Header = node.Name.Contains("#") ? node.InnerText : node.Name,
            };
            return item;
        }
    }
}

Here's the model:

using System.Xml;
using TableBuilder.Helpers;

namespace TableBuilder.Models
{
    public class XmlModel : ModelBase
    {
        private XmlDocument _xmlDoc;
        public XmlDocument XmlDoc
        {
            get { return _xmlDoc; }
            set
            {
                _xmlDoc = value;
                OnPropertyChanged("XmlDoc");
            }
        }

        public XmlModel()
        {
            XmlDoc = new XmlDocument();
        }
    }
}

Here's the viewmodel base class:

using System.ComponentModel;
using TableBuilder.Models;

namespace TableBuilder.Helpers
{
    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        static protected RedModel redModel { get; set; }
        static protected XmlModel xmlModel { get; set; }
        static protected MainModel mainModel { get; set; }

        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

And finally, here's the model base class:

using System.ComponentModel;

namespace TableBuilder.Helpers
{
    public class ModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

I know there are a lot of sloppy practices in the code; this is my first C#/WPF application. To help restrict the scope of this question, I just want to know why I can't set treeview data from my viewmodel. I've tried poking around Google, but haven't been successful. I should note that I was able to substitute the treeview with a textbox and set data to the textbox from the viewmodel without issue.

See the above comment section for the solution. elgonzo pointed out the major architectural issue that occurred during the rearchitecting process: view models do not directly interact with UI elements, they interact with data that is bound to the UI elements in the XAML.

The other missing piece was due to the fact that I was instantiating my viewmodels again in my mainviewmodel to create the appropriate aesthetic, however, this destroys the datacontext of the child windows, meaning I was only able to manipulate the data within the constructor. The solution was to instantiate the VIEWS when changing aspects of the UI, not the viewmodels. Doing this preserves the datacontext.

Edit: My XmlPane was not sending a notification that it was being updated. Instantiating my views over the viewmodel was a consequence of this, not what I had mentioned above.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM