-1

I'm trying to bind the <Visibility> attribute of a button to a boolean property of a custom class, but I'm failing to get any notifications back.

I have the following property in my viewmodel:

public MediaPlayerModel MediaPlayer { get; set; }

Where the type is written as follows:

public class MediaPlayerModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private MediaPlayerState _currentState;
    public MediaPlayerState CurrentState
    {
        get => _currentState;
        protected set
        {
            if (!Equals(value, _currentState))
            {
                _currentState = value;
                OnPropertyChanged();
            }
        }
    }

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
    }
}

And MediaPlayerState is declared as follows:

[Serializable]
public sealed class MediaPlayerState : BaseState
{
    public bool IsRunning { get; }

    //..
}

public class BaseState : IEquatable<BaseState> {..}

I then have the following xaml code:

<Button Content="Button" HorizontalAlignment="Left" Margin="212,337,0,0" VerticalAlignment="Top" Width="74"
        Visibility="{Binding MediaPlayer.CurrentState, Converter={converters:TestConverter}}">
</Button>

Where converters:TestConverter is implemented as follows:

[ValueConversion(typeof(MediaPlayerState), typeof(Visibility))]
public class TestConverter : BaseConverter, IValueConverter
{
    public object Convert(object value, Type targetType, object parameter,
        System.Globalization.CultureInfo culture)
    {
        var running = ((MediaPlayerState)value).IsRunning;
        if (running)
        {
            return Visibility.Visible;
        }
        return Visibility.Hidden;
    }

    public object ConvertBack(object value, Type targetType, object parameter,
        System.Globalization.CultureInfo culture)
    {
        return null;
    }
}

The visibility of the button never changes and the converter's method are never invoked.

I did come up with an alternative solution but it seems like a hack to me and not a proper fix and it opens a door for other potential bugs.

I'm binding the visibility attribute to the MediaPlayer property itself not the nested one:

Visibility="{Binding MediaPlayer... unchanged}

Than I'm subscribing to the PropertyChanged event from the MediaPlayerModel class during the Loaded event of the main window:

MediaPlayer.PropertyChanged += MediaPlayerOnPropertyChanged;

Where the event handler is implemented as follows:

private void MediaPlayerOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == nameof(MediaPlayer.CurrentState))
    {
        OnPropertyChanged(nameof(MediaPlayer));
    }
}

This makes it work, but it's pretty weird as this proofs that the event is actually fired at the right times, but the binding doesn't seems to work.

What is the cause of that? Why am I unable to bind to a nested property which has a working INotifyPropertyChanged event firing? And what's the fix to it?

Deadzone
  • 793
  • 1
  • 11
  • 33
  • Can you post your `INotifyPropertyChanged` implementation? Is it working for any other bindings? Also, is `OnPropertyChanged()` being called, i.e. is the value definitely changing/being set? – Chris Mack Nov 13 '17 at 00:53
  • @ChrisMack I included the implementation details, yes it is but that shouldn't be a problem. – Deadzone Nov 13 '17 at 00:57
  • If you haven't done so already, I'd try something more basic, like adding a Boolean property to the class and seeing if it will successfully bind to that. That will confirm that the `DataContext` is set correctly. – Chris Mack Nov 13 '17 at 01:04
  • Yeah I already tried that, tho that's not a good testing solution as I'm not really setting the value there so I cant fire the even at the right times, I do have a working alternative but it seems like a hack I will update the question to include it. – Deadzone Nov 13 '17 at 01:05
  • What was the result, did it bind? – Chris Mack Nov 13 '17 at 01:06
  • @ChrisMack Please take a look at the updated question. – Deadzone Nov 13 '17 at 01:10
  • 2
    `MediaPlayer.CurrentState` needs to implement INPC. You need to bind to `MediaPlayer.CurrentState.IsRunning` with a bool-to-visibility converter. Don't get clever. Just 1) name the property that will change in the Path for the Binding, so the Binding will know what to listen for; and 2) The class that owns the property that changes must implement INPC and raise PropertyChanged for the property that changes. If you try to outsmart the framework. you may succeed -- congratulations! But don't complain when you get your wish. – 15ee8f99-57ff-4f92-890c-b56153 Nov 13 '17 at 01:17
  • Agree with @EdPlunkett - I had to reread the code, I was reading `MediaPlayerState` as an `enum`, *sigh*. – Chris Mack Nov 13 '17 at 01:28
  • @EdPlunkett Yes, but `IsRunning` never changes, it's readonly, the only thing that changes is the `CurrentState`. The class itself works like an enum. – Deadzone Nov 13 '17 at 01:28
  • @Deadzone Hmmm. That’s a different kettle of fish. And you’re assigning a new Instance of MediaPlayerState to CurrentState, with a different value of IsRunning? – 15ee8f99-57ff-4f92-890c-b56153 Nov 13 '17 at 01:32
  • @EdPlunkett Correct. I'm not instantiating it on the spot tho, I use the already created ones from a static class, but let's not get into those details. – Deadzone Nov 13 '17 at 01:32
  • Is it getting past the recursion guard in the setter? Confirmed with a breakpoint? – 15ee8f99-57ff-4f92-890c-b56153 Nov 13 '17 at 01:34
  • @EdPlunkett Yes, my alternative solution also verifies that as I'm looking for the same property name. – Deadzone Nov 13 '17 at 01:35
  • @Deadzone BaseConverter is just a MarkupExtension subclass right? – 15ee8f99-57ff-4f92-890c-b56153 Nov 13 '17 at 01:40
  • @EdPlunkett Correct. – Deadzone Nov 13 '17 at 01:42
  • 1
    I've just built your solution and it's working for me in its current state. I'd put a breakpoint in the `set` and make sure it's definitely being called. – Chris Mack Nov 13 '17 at 01:42
  • Yes the set is being triggered I just triple-checked, also it's not logically possible for it to not be set, because there are things happening (playing,pausing) and all those things rely on the states. http://prntscr.com/h9oiav – Deadzone Nov 13 '17 at 01:47
  • Same as Chris here. Initially the button never appeared to appear because of that gargantuan margin pushing it out of sight, but it always hit TestConverter.Convert(). It seems extremely likely there's something going on in your code that's not in the picture you've shown us. Also I initially forgot to instantiate the viewmodel. That broke it. – 15ee8f99-57ff-4f92-890c-b56153 Nov 13 '17 at 01:50
  • The next thing I'd try is checking the value of `IsRunning` during the `set`, is it definitely changing? Also, could you post more of your xaml, or however you're setting the `DataContext` for the View. – Chris Mack Nov 13 '17 at 01:50
  • Something must be breaking the binding than, I'm setting up the property in my code behind through bindings like this http://prntscr.com/h9okd0 could that be causing the problem? Where `MediaPlayer` is declared like this http://prntscr.com/h9okk4 – Deadzone Nov 13 '17 at 01:55
  • It seems you have a `MediaPlayer` property on your `Window`, and then another in your ViewModel - two different instances? – Chris Mack Nov 13 '17 at 02:10

1 Answers1

0

The implementation of what you've posted that's working for me is:

<Window ... xmlns:vm="clr-namespace:MyApp.ViewModels" ... >
    <Window.DataContext>
        <vm:ViewModelContainingMediaPlayer />
    </Window.DataContext>
        ...
        <Button Visibility="{Binding MediaPlayer.CurrentState, Converter={StaticResource TestConverter}}" ... />
        ...
</Window>
Chris Mack
  • 5,148
  • 2
  • 12
  • 29
  • That's exactly what I have. – Deadzone Nov 13 '17 at 02:30
  • Hmmm, based on the last comment you wrote above, is it definitely the MediaPlayer on the ViewModel that's changing? It seemed like you have more than one? I.e. my implementation above wouldn't require the registration of any dependency properties. – Chris Mack Nov 13 '17 at 02:34
  • Yes, im setting it once at start and I use the one in thr vm after – Deadzone Nov 13 '17 at 02:46
  • Ok, I've implemented that and it's still working for me. Did you try checking the values of `IsRunning` when the `set` accessor is called? – Chris Mack Nov 13 '17 at 03:14