0

I am trying to dynamically populate a WPF tree by using a ViewModel, however, for some reason it's not working. Either the bindings either aren't properly or I am messing up somewhere in code behind.

Here's a sample of what I have. In XAML I define my TreeView like so...

<TreeView DockPanel.Dock="Left" Width="200" DataContext="{Binding MessageTree}" ItemsSource="{Binding MessageTree}">
    <TreeView.ItemContainerStyle>
            <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
            <Setter Property="FontWeight" Value="Normal" />
            <Style.Triggers>
                <Trigger Property="IsSelected" Value="True">
                    <Setter Property="FontWeight" Value="Bold" />
                </Trigger>
            </Style.Triggers>
        </Style>
        </TreeView.ItemContainerStyle>
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate DataType="viewModel:Mail" ItemsSource="{Binding Children}">
            <TextBlock Text="{Binding Subject}" />
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

In Code Behing i have...

private Mail MessageTree { get; set; }

And

using (var mail = new MailParser())
{
    int count = mail.GetMessageCount(DateTime.Today.AddDays(-10), DateTime.Today.AddDays(1));
    MessageTree = new Mail();
    for (int i = count - 1; i >= 0; i--)
    {
        MailMessage msg = mail.RetrieveMessage(i);
        if (msg != null)
        {
            MessageTree.Add(msg);
        }
        if (backgroundWorker != null)
        {
            decimal perc = (100.0m - (((i + 1.0m)*100.0m)/count));
            backgroundWorker.ReportProgress((int) perc, "Recebendo mensagens... " + perc.ToString("N2") + "%");
            if (backgroundWorker.CancellationPending)
            {
                e.Cancel = true;
                break;
            }
        }
    }
}

Mail is defined as

public sealed class Mail : INotifyPropertyChanged
{
    private readonly ObservableCollection<Mail> _children;
    private readonly MailMessage _msg;
    private readonly Mail _parent;

    private bool _isExpanded;
    private bool _isSelected;

    public Mail()
    {
        _msg = new MailMessage {Subject = "Empty"};
        _parent = null;
        _children = new ObservableCollection<Mail>();
    }

    public Mail(MailMessage msg, Mail parent = null)
    {
        _msg = msg;
        _parent = parent;
        _children = new ObservableCollection<Mail>();
    }

    public IEnumerable<Mail> Children
    {
        get { return _children; }
    }

    public string Subject
    {
        get { return _msg.Subject; }
    }

    public bool IsExpanded
    {
        get { return _isExpanded; }
        set
        {
            if (value != _isExpanded)
            {
                _isExpanded = value;
                OnPropertyChanged();
            }
            if (_isExpanded && _parent != null)
                _parent.IsExpanded = true;
        }
    }

    public bool IsSelected
    {
        get { return _isSelected; }
        set
        {
            if (value != _isSelected)
            {
                _isSelected = value;
                OnPropertyChanged();
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void Add(MailMessage msg)
    {
        _children.Add(new Mail(msg, this));
        OnPropertyChanged("Children");
    }

    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

I can't find anything in it so different from examples found online that it wouldn't work. The Add method is incomplete, I still need some logic to decide whether to add them to the collection or to the collection of one of the collection members, but as is all my Mail objecys are beeing added to the collection but not showing up in the TreeView.

What totally obvious thing am i missing? Shouldn't the TreeView automaticly update as I add items to the collection?

What I want is for the TreeView to show The children of the MessageTree property, and those children's children.

Cœur
  • 37,241
  • 25
  • 195
  • 267
537mfb
  • 1,374
  • 1
  • 16
  • 32

3 Answers3

1

Without seeing the entire setup, I'm not positive but my guess would be that since MessageTree is a plain CLR property (rather than something that raises PropertyChanged or a DependencyProperty or something, that the binding is occurring before your MessageTree = new Mail(); call. When you set it to a new instance, the binding system isn't getting notified since it is a plain property.

Another potential issue is that you say that code is in the code-behind. Just using that Binding syntax won't pick up a property from the code-behind. It's possible that you're setting that up somewhere else in the code that you didn't show us. But generally you aren't going to be binding from the View to the code-behind, you'd be binding to a ViewModel that was used as the DataContext for the view itself.

Tim
  • 14,999
  • 1
  • 45
  • 68
  • MessageTree is of Type Mail wich is my ViewModel and does implement INotifyPropertyChanged - if that's what you mean. – 537mfb Dec 28 '12 at 15:11
  • As for the new Mail() line, that's a good point - where would i initialize it then? i did try setting base.DataContext to MessageTree right after i call new on it but that didn't work – 537mfb Dec 28 '12 at 15:12
  • Ok gave you +1 cause you lead me half way to the answer - adding DataContext = MessageTree.Children; made me have to place the MessageTree.Add line inside a dispatcher invoke because of the binding - still not showing anything in the TreeView though – 537mfb Dec 28 '12 at 15:24
1

EDIT: Couldn't see the whole thing on my phone - amended answer based on ability to actually see everything. :)

MOREEDIT: updated based on comments, let's start from scratch!

First off, if you're set on using the window/whatever as the datacontext, let's make it `INotifyPropertyChange...next, let's make "MessageTree" a collection of mails, not just a single one (it'll make binding semantics easier, trust me)

public class WhateverContainsTheTree : Window, INotifyPropertyChanged
{
    public WhateverContainsTheTree()
    {
        this.Loaded += OnLoaded;
        this._messageTree = new ObservableCollection<Mail>();
        this.DataContext = this;
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        _worker = new BackgroundWorker();
        _worker.DoWork += WorkerWorkin;
        _worker.RunWorkerAsync();
    }

    private BackgroundWorker _worker;
    private ObservableCollection<Mail> _messageTree;    
    public ObservableCollection<Mail> MessageTree 
    { 
        get { return _messageTree; }  
        set { _messageTree = value; RaisePropertyChanged("MessageTree"); } 
    }

    public event PropertyChangedEventHandler PropertyChanged = delegate {};
    private void RaisePropertyChanged(string propertyName)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    private void WorkerWorkin(object sender, DoWorkEventArgs e)
    {
                // obviously, change this to your stuff; I added a ctor so I could pass a string
        Thread.Sleep(3000);
        Console.WriteLine("Ok, setting message tree");
        Dispatcher.Invoke(
            System.Windows.Threading.DispatcherPriority.Normal,
            (Action)(() => 
            { 
                var mail1 = new Mail("Mail#1:Changed from background thread");
                var mail2 = new Mail("Mail#2:Submail of mail #1");
                var mail3 = new Mail("Mail#3:Submail of mail #2");
                var mail4 = new Mail("Mail#4:Submail of mail #1");
                var mail5 = new Mail("Mail#5:Submail of mail #4");
                mail1.Children.Add(mail2);
                mail1.Children.Add(mail4);
                mail2.Children.Add(mail3);
                mail4.Children.Add(mail5);
                MessageTree.Add(mail1);
            })
        );
    }
}

Also, like I'd said in the original response, let's slightly tweak Mail.Children:

public ObservableCollection<Mail> Children
{
    get { return _children; }
}

And here's what I used for the treeview xaml:

<TreeView DockPanel.Dock="Left" Width="200" ItemsSource="{{Binding MessageTree}}">
    <TreeView.ItemContainerStyle>
            <Style TargetType="{{x:Type TreeViewItem}}">
            <Setter Property="IsExpanded" Value="{{Binding IsExpanded, Mode=TwoWay}}" />
            <Setter Property="IsSelected" Value="{{Binding IsSelected, Mode=TwoWay}}" />
            <Setter Property="FontWeight" Value="Normal" />
            <Style.Triggers>
                <Trigger Property="IsSelected" Value="True">
                    <Setter Property="FontWeight" Value="Bold" />
                </Trigger>
            </Style.Triggers>
        </Style>
        </TreeView.ItemContainerStyle>
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate DataType="viewModel:Mail" ItemsSource="{{Binding Children}}">
                <TextBlock Text="{{Binding Subject}}" />
            </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

If this STILL doesn't work, I'll just paste in the whole LINQPad blob I put together to test this.

JerKimball
  • 16,584
  • 3
  • 43
  • 55
  • Shouldn't the datacontext be what's holding the items anyway? in this case MessageTree? – 537mfb Dec 28 '12 at 16:19
  • Aha - that's what I get for looking at this on a phone...part of it was cut off. Lemme amend my answer... – JerKimball Dec 28 '12 at 16:24
  • Nope - didn't work - What ended up working was giving the treeview a name (Tree) and after the MessageTree = new Mail(); line set Tree.ItemsSource = MessageTree.Children; – 537mfb Dec 28 '12 at 17:01
  • I don't have time to try this right now - time to leave work and go home - but looking at that makes me raise a few questions - 1) Why make window Inotifyable? isn't the all point of ViewModel to separate that? shouldn't i only have in window a reference to the collection to use? - in my case MessageTree holded a ViewModel with the ViewModel collection to host. I have other questions but time is short. Could you explain how you would go about this without making window INotifyable? thnx - Monday will test your solution – 537mfb Dec 28 '12 at 18:44
  • Re: the notifyable/viewmodel thing - you are absolutely correct, but since you are *setting* your viewmodel class from a background thread, you're forcing the window to also act like a view model - if you were to, say, set the MessageTree property on construction, then *populate* it in the background thread, that would be "more correct" - and also way more MVVM. – JerKimball Dec 28 '12 at 18:50
  • Setting it in constructor was one of the things i have tryed - didn't work - wich is way i tryed the other things - like i said, i must have something fundamentally wrong that's illuding me – 537mfb Dec 31 '12 at 10:20
  • So i moved my MessageTree = new Mail(); to my window Constructor and added Tree.ItemsSource = MessageTree.Children; below it. Even though i have ItemsSource="{Binding MessageTree.Children}" in my XAML it doesn't bind on it's own - adding that to the constructor after initialization does make that work - not sure why the XAML alone doesn't do it. – 537mfb Dec 31 '12 at 10:28
  • The accepted answer in http://stackoverflow.com/questions/2147559/wpf-setting-itemsource-in-xaml-vs-code-behind lead me to change the XAML to ItemsSource="{Binding MessageTree.Children, RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}" - In other words, bind through the window ancestor - not perfect but don't have to set itemsSource in code behind window anymore - will accept your answer since all your comments led me to this answer - really need to understand MVVM – 537mfb Dec 31 '12 at 10:44
  • @537mfb Interesting...that would imply there's a good deal more to the XAML other than what you posted. At any rate, I'm glad your problem is solved; for any future questions, I'd put together a "stripped down" version of the full xaml (i.e., from window down to problem location), as it helps troubleshooting considerably. Cheers! – JerKimball Dec 31 '12 at 16:22
  • the XAML i posted is all that relates to the TreeView - there's nothing else elsewhere related to it - the only thing i can see right now that i missed out in pointing out is that i have that XAML piece of code in the Window XAML - i have since seen than usually controls are placed in views of their own called WorkSpaces - other than that, what i showed here is the stripped down version of what i had – 537mfb Jan 03 '13 at 12:13
0

Had to give a name to the TreeView (Tree) and then after

MessageTree = new Mail();

insert

Tree.ItemSource = MessageTree.Children;

I find this ugly but at least it works now. Thank you all for trying to help.

537mfb
  • 1,374
  • 1
  • 16
  • 32
  • That's really strange...where exactly are you doing the MessageTree = new Mail() call? I see the using... block, but where is this actually happening? – JerKimball Dec 28 '12 at 17:07
  • in a backgroundworker - the backgroundworker is starter on the window_loaded event and is used to populate the ViewModel using mail messages from a gmail account – 537mfb Dec 28 '12 at 17:15
  • Aha! That explains some things...lemme take another whack at amending my answer... – JerKimball Dec 28 '12 at 17:30
  • Be my guest - if you find a better solution then mine posted as a new answer and i will mark it – 537mfb Dec 28 '12 at 18:35