1

I've added a ScrollViewer behavior that allows me to scroll to the top when a property is set to true based on Scroll the scrollviewer to top through viewmodel. I've found that this works perfectly the first time, but subsequent attempts do not fire because the TwoWay binding isn't setting my property back to false.

Here is my simple project showing my issue:

MainWindow.xaml

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="100" Width="200">
    <ScrollViewer local:ScrollViewerBehavior.ScrollToTop="{Binding Reset, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="200"/>
            </Grid.RowDefinitions>
            <Button VerticalAlignment="Bottom" Content="Top" Command="{Binding Scroll}"/>
        </Grid>
    </ScrollViewer>
</Window>

MainWindow.xaml.cs

using System.Windows;

namespace WpfApp1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private ViewModel _ViewModel;

        public MainWindow()
        {
            InitializeComponent();

            DataContext = _ViewModel = new ViewModel();
        }
    }
}

ViewModel.cs

using System;
using System.ComponentModel;
using System.Windows.Input;

namespace WpfApp1
{
    public class RelayCommand : ICommand
    {
        private readonly Action<object> action;

        public RelayCommand(Action<object> action)
        {
            this.action = action;
        }

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        {
            return true;
        }

        public void Execute(object parameter)
        {
            action(parameter);
        }
    }

    public class ViewModel : INotifyPropertyChanged
    {
        private bool _reset = false;

        public ViewModel()
        {
            Scroll = new RelayCommand(o =>
            {
                Reset = true;
            });
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public bool Reset
        {
            get { return _reset; }
            set
            {
                bool changed = value != _reset;
                _reset = value;

                if (changed)
                {
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Reset)));
                }
            }
        }

        public RelayCommand Scroll { get; set; }
    }
}

ScrollViewerBehavior.cs

using System.Windows;
using System.Windows.Controls;

namespace WpfApp1
{
    public static class ScrollViewerBehavior
    {
        public static readonly DependencyProperty ScrollToTopProperty = DependencyProperty.RegisterAttached("ScrollToTop", typeof(bool), typeof(ScrollViewerBehavior), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (o, e) =>
        {
            if (o is ScrollViewer sc)
            {
                if ((bool)e.NewValue)
                {
                    sc.ScrollToTop();
                    SetScrollToTop((FrameworkElement)o, false); // this should set the property back to false
                }
            }
        }));

        public static bool GetScrollToTop(FrameworkElement o)
        {
            return (bool)o.GetValue(ScrollToTopProperty);
        }

        public static void SetScrollToTop(FrameworkElement o, bool value)
        {
            o.SetValue(ScrollToTopProperty, value);
        }
    }
}

I know that if I take out the changed check on the property, it works; however, that is not ideal for my situation. When I look at the element through WPF Inspector, I see that the property on the ScrollViewer is false as it should be, but my ViewModel property remains true.

Middas
  • 1,862
  • 1
  • 29
  • 44
  • debug to find down? – Antoine V Jul 26 '18 at 20:44
  • @ThierryV, I have put break points in, when the line of code that should set the value back to false is executed, there is nothing on the ViewModel. WPF Inspector says it's updated though, I'm not sure what else to debug. – Middas Jul 26 '18 at 20:55
  • Hi, apperantly (I was stumbling on this a few times) the dependency property's Setvalue Method doesn't fire Notifypropertychanged as expexted. Maybe extending the binding of the behaviour with ' NotifyOnSourceUpdated=True , NotifyOnTargetUpdated=True' could help (although the latter won't happen, since you don't change the value on the bound control...) – dba Jul 27 '18 at 06:00
  • okay, rereading shows, you need the targetupdated :) – dba Jul 27 '18 at 06:10
  • @dba that's interesting to know that it doesn't fire the event. Is there a way around that? – Middas Jul 27 '18 at 16:23
  • Did the 'NotifyOn..' setting not work for your scenario? – dba Jul 27 '18 at 16:27
  • No, the value still doesn't propagate to the ViewModel. – Middas Jul 27 '18 at 16:29
  • Noob Question: where do you bind your Viewmodel as Datacontext? can't see it in this pieces of code – dba Jul 27 '18 at 16:33
  • The code behind of the xaml, I'll update the post with it. – Middas Jul 27 '18 at 16:34

1 Answers1

0

I'm not good in English but i wrote this example. Look at this

[MainWindow.xaml.cs]

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    AnyClass c = new AnyClass();

    private void h1_Click(object sender, RoutedEventArgs e)
    {
        Test = true;
    }
    private void h2_Click(object sender, RoutedEventArgs e)
    {
        Test = false;
    }

    public bool Test
    {
        get { return (bool)GetValue(TestProperty); }
        set
        {
            SetValue(TestProperty, value);
            c.Val = value;
        }
    }
    public static readonly DependencyProperty TestProperty =
        DependencyProperty.Register("Test", typeof(bool), typeof(MainWindow), new PropertyMetadata(false));


}

[AnyClass.cs]
class AnyClass
{
    private bool val = false;
    public bool Val
    {
        get
        {
            return val;
        }
        set
        {
            val = value;
        }
    }
}

[mainWindow.xaml]
<Button Click="h1_Click" Content="true">
            <Button.Style>
                <Style TargetType="Button">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding ElementName=hUserControl, Path=Test}" Value="True">
                            <Setter Property="Visibility" Value="Collapsed"/>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding ElementName=hUserControl, Path=Test}" Value="False">
                            <Setter Property="Visibility" Value="Visible"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Button.Style>
        </Button>
        <Button Click="h2_Click" Content="false">
            <Button.Style>
                <Style TargetType="Button">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding ElementName=hUserControl, Path=Test}" Value="True">
                            <Setter Property="Visibility" Value="Visible"/>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding ElementName=hUserControl, Path=Test}" Value="False">
                            <Setter Property="Visibility" Value="Collapsed"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Button.Style>
        </Button>
  • Your example does not demonstrate what I'm trying to do. The problem is the behavior sets the value back to false, but the ViewModel doesn't get that value. – Middas Jul 27 '18 at 16:22