1

This question is a follow up to this question. My overall goal at the moment is to add to my program's TreeViewItem (my TreeViewItem has child nodes added to it at run-time) in numerical ascending order according to the value entered in for the header.

I received an answer using a ModelView, a tool that I am not all that familiar with, and I was also told that this could be done by using a List of TreeViewItems. I have decided to explore the List option due to my lack of experience with ModelView.

In my research I've learned that Lists of TreeViewItems are a little different, because you really can't reference them like you can an array. This makes them more difficult to do work with. I'll explain my current method and post my code. Please steer me in the right direction and provide answers with coding solutions. I'm currently stuck on my treeViewListAdd function. I have written pseudo code in comments on what I am trying to do with that area.

*Note: I am adding to my TreeViewItem from a separate window

Right now my add TreeViewItem process consists of:

  1. Check to see if item entered is numerical (DONE)
  2. if not numerical, break operation (DONE)
  3. else -- continue with adding child item (DONE)
  4. Check for duplicate children (DONE)
  5. if duplicate is found break operation (DONE)
  6. else -- continue (DONE)
  7. Create List of TreeViewItem (DONE -- But not implemented)
  8. Create TreeViewItem for new child node (DONE)
  9. TVI header is set from user text in textBox (DONE)
  10. Pass to function that attempts to add to the List in numerical order (Problem Area)
  11. Add sorted List to TreeViewItem in main window (Problem Area)

My current code:

//OKAY - Add child to TreeViewItem in Main Window
private void button2_Click(object sender, RoutedEventArgs e)
{
    //STEP 1: Checks to see if entered text is a numerical value
    string Str = textBox1.Text.Trim();
    double Num;
    bool isNum = double.TryParse(Str, out Num);

    //STEP 2: If not numerical value, warn user
    if (isNum == false)
        MessageBox.Show("Value must be Numerical");
    else //STEP 3: else, continue
    {
        //close window
        this.Close();

        //Query for Window1
        var mainWindow = Application.Current.Windows
            .Cast<Window1>()
            .FirstOrDefault(window => window is Window1) as Window1;

        //STEP 4: Check for duplicate
        //declare TreeViewItem from mainWindow
        TreeViewItem locations = mainWindow.TreeViewItem;
        //Passes to function -- checks for DUPLICATE locations
        CheckForDuplicate(locations.Items, textBox1.Text);

        //STEP 5: if Duplicate exists -- warn user
        if (isDuplicate == true)
            MessageBox.Show("Sorry, the number you entered is a duplicate of a current Node, please try again.");
        else //STEP 6: else -- create child node
        {
            //STEP 7
            List<TreeViewItem> treeViewList = new List<TreeViewItem>();

            //STEP 8: Creates child TreeViewItem for TVI in main window
            TreeViewItem newLocation = new TreeViewItem();

            //STEP 9: Sets Headers for new child nodes
            newLocation.Header = textBox1.Text;

            //STEP 10: Pass to function -- adds/sorts List in numerical ascending order
            treeViewListAdd(ref treeViewList, newLocation);

            //STEP 11: Add children to TVI in main window
            //This step will of course need to be changed to add the list
            //instead of just the child node
            mainWindow.TreeViewItem.Items.Add(newLocation);
        }
    }
}

//STEP 4: Checks to see whether the header entered is a DUPLICATE
private void CheckForDuplicate(ItemCollection treeViewItems, string input)
{
        for (int index = 0; index < treeViewItems.Count; index++)
        {
            TreeViewItem item = (TreeViewItem)treeViewItems[index];
            string header = item.Header.ToString();

            if (header == input)
            {
                isDuplicate = true;
                break;
            }
            else
                isDuplicate = false;
        }
}

//STEP 10: Adds to the TreeViewItem list in numerical ascending order
private void treeViewListAdd(ref List<TreeViewItem> currentList, TreeViewItem addLocation)
{
        //if there are no TreeViewItems in the list, add the current one
        if (currentList.Count() == 0)
            currentList.Add(addLocation);
        else
        {
            //gets the index of the last item in the List
            int lastItem = currentList.Count() - 1;

            /*
            if (value in header > lastItem)
                currentList.Add(addLocation);
            else
            {
                //iterate through list and add TreeViewItem
                //where appropriate
            }
            **/
        }
}

Thanks a lot for the help. I tried to show that I have been working on this and trying everything that I could on my own.

As requested, here is the structure of my TreeView. Everything from the 3rd level and down is dynamically added by the user...

enter image description here

Community
  • 1
  • 1
Eric after dark
  • 1,768
  • 4
  • 31
  • 79
  • dude, for God's sake create a ViewModel and remove all that horrible code, please. I mean post a screenshot of what you need and I can tell you the proper way to do it in WPF. – Federico Berasategui Jul 30 '13 at 14:30
  • Not really sure how to do that man. – Eric after dark Jul 30 '13 at 14:32
  • I just read your previous question, and you got a proper MVVM answer. Why didn't you follow that answer? – Federico Berasategui Jul 30 '13 at 14:38
  • Well, I said above that I wanted to try the `List` because I didn't really understand the MVVM answer. I thought it would be simpler. – Eric after dark Jul 30 '13 at 14:42
  • no it's not "simpler" in any way. The WPF Visual Tree is a really complex thing you don't want to get into. The winforms approach of manipulating UI elements in procedural code is useless in WPF. The MVVM pattern simplifies all this by providing an abstraction of the View and using DataBinding to separate the View from the application logic and data. – Federico Berasategui Jul 30 '13 at 14:53
  • Alright, I'll focus on the MVVM approach then. Do you want to provide an answer, or should I delete this question and use the answer from the other question? – Eric after dark Jul 30 '13 at 14:58
  • I'm finishing the sample right now. – Federico Berasategui Jul 30 '13 at 15:02
  • 1
    Just so you know, you *can* access the items in a `List` using indexers just like an array: `TreeViewItem item = listOfTreeViewItems[0];` – Sheridan Jul 30 '13 at 16:08

1 Answers1

6

Ok. Delete all your code and start all over.

1: It is essential that you read up on MVVM before writing a single line of code in WPF.

You can read about it here and here and here

<Window x:Class="MiscSamples.SortedTreeView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cmp="clr-namespace:System.ComponentModel;assembly=WindowsBase"
        Title="SortedTreeView" Height="300" Width="300">
    <DockPanel>
        <TextBox Text="{Binding NewValueString}" DockPanel.Dock="Top"/>
        <Button Click="AddNewItem" DockPanel.Dock="Top" Content="Add"/>
        <TreeView ItemsSource="{Binding ItemsView}" SelectedItemChanged="OnSelectedItemChanged">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding ItemsView}">
                    <TextBlock Text="{Binding Value}"/>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </DockPanel>
</Window>

Code Behind:

public partial class SortedTreeView : Window
{
    public SortedTreeViewWindowViewModel ViewModel { get { return DataContext as SortedTreeViewWindowViewModel; } set { DataContext = value; } }

    public SortedTreeView()
    {
        InitializeComponent();
        ViewModel = new SortedTreeViewWindowViewModel()
            {
                Items = {new TreeViewModel(1)}
            };
    }

    private void AddNewItem(object sender, RoutedEventArgs e)
    {
        ViewModel.AddNewItem();
    }

    //Added due to limitation of TreeViewItem described in http://stackoverflow.com/questions/1000040/selecteditem-in-a-wpf-treeview
    private void OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        ViewModel.SelectedItem = e.NewValue as TreeViewModel;
    }
}

2: First thing you can notice above is that the Code Behind does NOTHING. It just delegates functionality to something called the ViewModel (not ModelView, which is a misspelling)

Why is that?

Because it's a much better approach. Period. Having the application logic / business logic and data separated and decoupled from the UI is the best thing to ever happen to any developer.

So, What's the ViewModel about?

3: The ViewModel exposes Properties that contain the Data to be shown in the View, and Methods that contain the logic to operate with the Data.

So it's as simple as:

public class SortedTreeViewWindowViewModel: PropertyChangedBase
{
    private string _newValueString;
    public int? NewValue { get; set; }

    public string NewValueString
    {
        get { return _newValueString; }
        set
        {
            _newValueString = value;
            int integervalue;

            //If the text is a valid numeric value, use that to create a new node.
            if (int.TryParse(value, out integervalue))
                NewValue = integervalue;
            else
                NewValue = null;

            OnPropertyChanged("NewValueString");
        }
    }

    public TreeViewModel SelectedItem { get; set; }

    public ObservableCollection<TreeViewModel> Items { get; set; }

    public ICollectionView ItemsView { get; set; }

    public SortedTreeViewWindowViewModel()
    {
        Items = new ObservableCollection<TreeViewModel>();
        ItemsView = new ListCollectionView(Items) {SortDescriptions = { new SortDescription("Value",ListSortDirection.Ascending)}};
    }

    public void AddNewItem()
    {
        ObservableCollection<TreeViewModel> targetcollection;

        //Insert the New Node as a Root node if nothing is selected.
        targetcollection = SelectedItem == null ? Items : SelectedItem.Items;

        if (NewValue != null && !targetcollection.Any(x => x.Value == NewValue))
        {
            targetcollection.Add(new TreeViewModel(NewValue.Value));
            NewValueString = string.Empty;    
        }

    }
}

See? All your 11 requirements are fulfilled by 5 lines of code in the AddNewItem() method. No Header.ToString() stuff, no casting anything, no horrible code behind approaches.

Just simple, simple properties and INotifyPropertyChanged.

And what about the sorting?

4: The sorting is performed by the CollectionView, and it occurs at the ViewModel level, not at the View Level.

Why?

Because Data is kept separate from it's visual representation in the UI.

5: Finally, here is the actual Data Item:

public class TreeViewModel: PropertyChangedBase
{
    public int Value { get; set; }

    public ObservableCollection<TreeViewModel> Items { get; set; }

    public CollectionView ItemsView { get; set; }

    public TreeViewModel(int value)
    {
        Items = new ObservableCollection<TreeViewModel>();
        ItemsView = new ListCollectionView(Items)
            {
                SortDescriptions =
                    {
                        new SortDescription("Value",ListSortDirection.Ascending)
                    }
            };
        Value = value;
    }
}

This is the class that will hold the data, in this case, an int Value, because you only care about numbers, so that's the right data type, and then an ObservableCollection that will hold the child nodes, and again the CollectionView that takes care of the sorting.

6: Whenever you use DataBinding in WPF (which is essential to all this MVVM thing) you need to implement INotifyPropertyChanged, so this is the PropertyChangedBase class All ViewModels inherit from:

public class PropertyChangedBase:INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        Application.Current.Dispatcher.BeginInvoke((Action) (() =>
                                                                 {
                                                                     PropertyChangedEventHandler handler = PropertyChanged;
                                                                     if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
                                                                 }));
    }
}

And wherever there's a change in a property you need to Notify that by doing:

OnPropertyChanged("PropertyName");

as in

OnPropertyChanged("NewValueString");

And this is the result:

enter image description here

  • Just copy and paste all my code in a File -> New Project -> WPF Application and see the results for yourself.

  • Let me know if you need me to clarify anything.

Federico Berasategui
  • 43,562
  • 11
  • 100
  • 154
  • I have added the following using directives: `System.ComponentModel;`, and `System.Collections.ObjectModel;`. This should implement `INotifyPropertyChanged`, just like you said to do. However, the compiler does not recognize `PropertyChangedBase`. Why is this, and how can I fix it? – Eric after dark Jul 30 '13 at 17:20
  • @EricAfterDark PropertyChangedBase is included in the answer in Part 6. Make sure you include that too. – Federico Berasategui Jul 30 '13 at 17:20
  • Got it. Wow, this is really an amazing solution that I can definitely work my program around. How would I do UI things with this View Model, such as, making sure that a `Labeled` `TreeViewItem` always exists through level 3 at program start? Like if I want the `TreeView` to have a parent, child, and grandchild at startup? – Eric after dark Jul 30 '13 at 17:25
  • @EricAfterDark it's all data, and then the UI is manifested via DataBinding by WPF, just make sure you provide the appropriate data and WPF will take care of the UI. This is, make sure you provide the right `TreeViewModel`s with the right `Values`. – Federico Berasategui Jul 30 '13 at 17:26
  • Okay, so the `HierarchicalDataTemplate` is pretty much the only thing that will ever be in the design window? Everything about the `TreeView` comes from the data section then. – Eric after dark Jul 30 '13 at 17:31
  • @ericAfterDark Data is brought about by ViewModels, in the form of "Data Items", and the View (XAML) only cares about "what to show" for that data, and not "how to deal" with the data. – Federico Berasategui Jul 30 '13 at 17:37
  • what if I want to insert another `TreeView` below the current one? Whenever I add it to the .XAML window it always places it on the right of the current `TreeView`. – Eric after dark Jul 30 '13 at 17:50
  • @EricAfterDark You need to modify the existing layout. I used a DockPanel because I only needed these UI elements, but if you need a more complex layout you may want to use the proper element, maybe a `Grid`, maybe something else. But that is a separate question in and of itself, don't you think? – Federico Berasategui Jul 30 '13 at 17:52