0

I'm using MVVMLight. This is what I have from an example here.

private ObservableCollection<Inline> _inlineList;

public ObservableCollection<Inline> InlineList
{
  get { return _inlineList; }
  set { Set(() => InlineList, ref _inlineList, value); }
}

private void SendClicked()
{
    InlineList.Add(new Run("This is some bold text") { FontWeight = FontWeights.Bold });
    InlineList.Add(new Run("Some more text"));
    InlineList.Add(new Run("This is some text") { TextDecorations = TextDecorations.Underline });
}

public class BindableTextBlock : TextBlock
{

    public ObservableCollection<Inline> InlineList
    {
        get { return (ObservableCollection<Inline>)GetValue(InlineListProperty); }
        set { SetValue(InlineListProperty, value); }
    }

    public static readonly DependencyProperty InlineListProperty =
        DependencyProperty.Register("InlineList", typeof(ObservableCollection<Inline>), typeof(BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));

    private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue == null) return;

        var textBlock = (BindableTextBlock)sender;
        textBlock.Inlines.AddRange((ObservableCollection<Inline>)e.NewValue);
    }
}

    <testRobot:BindableTextBlock  
                                 Width="Auto" Height="Auto" 
                                 InlineList="{Binding InlineList}" >

    </testRobot:BindableTextBlock>

The problem is that bound property InlineList never gets updated. I don't see any text I add to the collection ObservableCollection. When I put a break point in OnPropertyChanged method it never gets hit. I know my data context is set correctly as other bound controls work. What could be the problem?

Community
  • 1
  • 1
KrisW
  • 165
  • 1
  • 1
  • 13
  • This is not C! Do not add C tag for non-C code – too honest for this site Oct 28 '15 at 14:48
  • My answer got too big so here's what I think is happening in your code: You must have some place that you're initializing the observable collection (that is missing in the provided code, otherwise it will throw null reference on the add), and when you do that and use Set method, in the property's setter the binding is done, and after that nothing is fired because you don't change the reference to the collection but you just edit it's values. – kirotab Oct 28 '15 at 19:58

1 Answers1

-1

Ok you only need to add these in your BindableTextBlock With your Original solution. What we do here is we add handler for when collection is changed (meaning new values are added), we only do that when the collection is set. So with the binding that you have in your xaml every change you make on the collection in the VM fires collection changed event in the textblock which in turn just appends values to the Inline.

private void CollectionChangedHandler(object sender, NotifyCollectionChangedEventArgs e)
{
    Inlines.AddRange(e.NewItems);
}

private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    if (e.NewValue == null) return;

    var textBlock = (BindableTextBlock)sender;
    textBlock.InlineList.CollectionChanged += textBlock.CollectionChangedHandler;
}

BEFORE EDIT for history reasons

Ok I saw what's happening so first the explanation then an example.

So first some basic concepts about wpf:

In order to have your view notified for a change in bound variable in your ViewModel (or whatever that is DataContext at the moment) you have to either RaisePropertyChanged event with the name of the changed property or use something that's doing this somehow :) - like ObservableCollection.

So some use cases:

You have a property with field -> it is common practice to have it like this:

private ICollection<Inline> _inlineList;

public ICollection<Inline> InlineList
{
    get
    {
        return _inlineList;
    }
    set
    {
        _inlineList = value;
        RaisePropertyChanged("InlineList");
    }

}

This ensures that when you set a new value to InlineList the view will be notified

Or in your case what I've used:

private ICollection<Inline> _inlineList;

public ICollection<Inline> InlineList
{
    get { return _inlineList; }
    set { Set(() => InlineList, ref _inlineList, value); }
}

If you check the description of Set method you'll see that it is setting the value and raising the property (and some more stuff)

You want to have automatic updates and use ObservableCollection -> I use it like this:

private ObservableCollection<ClientFilter> clientFilters;

public IEnumerable<ClientFilter> ClientFilters
{
    get
    {
        if (this.clientFilters == null)
        {
            this.clientFilters = new ObservableCollection<ClientFilter>();
        }

        return this.clientFilters;
    }
    set
    {
        if (this.clientFilters == null)
        {
            this.clientFilters = new ObservableCollection<ClientFilter>();
        }

        SetObservableValues<ClientFilter>(this.clientFilters, value);
    }
}

The method SetObservableValues is in my main ViewModel and is doing this:

public static void SetObservableValues<T>(
    ObservableCollection<T> observableCollection,
    IEnumerable<T> values)
{  
    if (observableCollection != values)
    {
        observableCollection.Clear();

        foreach (var item in values)
        {
            observableCollection.Add(item);
        }
    }
}

This method ensures that if the reference to the obs collection is not the same it will clear the old one and reuse it, because when you bind you bind to the reference at common mistake is to then change the reference itself not the values, which in turn doesn't update anything on the UI and you think binding is broken :)

So If you want it to function normally you just add/remove to the Collection/Enumerable ClientFilters

Now the solution

So I'm not 100% sure what you want to achieve but here's what you could do in order to have your binding working

Your ViewModel

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Documents;

namespace WpfApplication3.ViewModel
{
public class MainViewModel : ViewModelBase
{
    public MainViewModel()
    {

    }

    private ICollection<Inline> _inlineList;

    public ICollection<Inline> InlineList
    {
        get { return _inlineList; }
        set { Set(() => InlineList, ref _inlineList, value); }
    }

    public RelayCommand SendClicked
    {
        get
        {
            return new RelayCommand(() =>
            {
                InlineList = new List<Inline>
                {
                    new Run("This is some bold text") { FontWeight = FontWeights.Bold },
                    new Run("Some more text"),
                    new Run("This is some text") { TextDecorations = TextDecorations.Underline }
                };
            });
        }
    }
}
}

Your custom control -> BindableTextBlock

using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;

namespace WpfApplication3
{
    public class BindableTextBlock : TextBlock
    {
        public ICollection<Inline> InlineList
        {
            get { return (ICollection<Inline>)GetValue(InlineListProperty); }
            set { SetValue(InlineListProperty, value); }
        }

        public static readonly DependencyProperty InlineListProperty =
            DependencyProperty.Register("InlineList", typeof(List<Inline>), typeof(BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));

        private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue == null) return;

            var textBlock = (BindableTextBlock)sender;

            textBlock.Inlines.AddRange((ICollection<Inline>)e.NewValue);
        }
    }
}

Your XAML

On your Page or Window (depending on platform)

DataContext="{Binding Main, Source={StaticResource Locator}}"

Then inside

<StackPanel>
    <Button Command="{Binding SendClicked}">SendClicked</Button>
    <local:BindableTextBlock Background="Black" Foreground="AliceBlue"
                             Width = "Auto" Height="Auto" 
                             InlineList="{Binding InlineList}" 
                             >
    </local:BindableTextBlock>
</StackPanel>

All assuming you have your ViewModelLocator from MVVM Light register your view model

using GalaSoft.MvvmLight.Ioc; using Microsoft.Practices.ServiceLocation;

namespace WpfApplication3.ViewModel
{
    public class ViewModelLocator
    {
        public ViewModelLocator()
        {
            ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

            SimpleIoc.Default.Register<MainViewModel>();
        }

        public MainViewModel Main
        {
            get
            {
                return ServiceLocator.Current.GetInstance<MainViewModel>();
            }
        }

        public static void Cleanup()
        {
        }
    }
}

ALTERNATIVE

Alternatively you could have your command like this:

public RelayCommand SendClicked
{
    get
    {
        return new RelayCommand(() =>
        {
            _inlineList = new List<Inline>();
            InlineList.Add(new Run("This is some bold text") { FontWeight = FontWeights.Bold });
            InlineList.Add(new Run("Some more text"));
            InlineList.Add(new Run("This is some text") { TextDecorations = TextDecorations.Underline });
            RaisePropertyChanged("InlineList");
        });

    }
}

But you have to use the other option of defining the property as described at the beginning of my post.

You could do it in other ways of course.

Just one more advice -> It is considered bad practice and not in the spirit of MVVM to have UI elements in your view model, so change in architecture is strongly advised in this code IMO.

Post got too long (as usual), if you need aditional explanation please let me know. Cheers and happy coding ;)

kirotab
  • 1,296
  • 1
  • 11
  • 18
  • 1
    Thanks for your reply. I don't see anything different between your solution and mine besides you using an ICollection and me using an Observable collection. I'm not using(directly anyways) RaisePropertyChanged in my view model because I'm using MVVM Light. The SendClicked() method executes when a button is pressed on the UI, the collection gets filled but this is not reflected in my view. – KrisW Oct 28 '15 at 20:42
  • This solution I've tested on a new empty project so check it out, you don't need to use Observable collection in your case because you notify the view on a certain event... However I found an issue with my answer so I'll make an edit and will get back to you with the solution ;) And by the way observable collection does not come from MVVM Light and you also have RaiseProperty changed in MVVM Light :) – kirotab Oct 28 '15 at 21:14
  • @KrisW Please try the code from my edit (at the top of the answer). I've tested it so it should be working for you too. If you have trouble let me know. – kirotab Oct 28 '15 at 21:52
  • Thanks for you reply. I did try your solution and it worked. What I require is for a method to be executed when someone presses the SendClicked button so I can construct an new Incline list based on criteria from the UI. Right now your way just return the same list. How can I accomplish this? – KrisW Oct 29 '15 at 13:26
  • @KrisW In this version I only fixed your binding, it's doing the same that you were doing before. It's better to close this question and open a new one for the new issue I think. So only a few tips for the new one 1: You could pass some parameters if you're using Command (like in my example), there is a lot of information how to use CommandParameters 2: You could pass some arguments via the sender object (using it's data context in the handler) 3: You could have some values that are accessible from the handler. But really post a new question or search a bit :) – kirotab Oct 29 '15 at 13:52