Ttxman / WpfRangeControls

Customized WPF controls, RangePanel with custom vertical/horizontal positioning, RangeItemsControl with bindable ItemsSource and Visual Studio-like scrollbar with markers
MIT License
24 stars 6 forks source link

How should I bind this? #1

Closed Dirkster99 closed 5 years ago

Dirkster99 commented 5 years ago

Hi,

I am trying to refactor my Diff Overview control (which inherites from Slider) shown on the left side of this screenshot: Ähnlich1

into a control version that is based on a Scrollbar since that seems to be more natural in terms of the control behavior. In that process I am trying to integrate your WpfRangeControl on this branch:

https://github.com/Dirkster99/Aehnlich/tree/Overview_Refactoring

where I have this per DiffLineViewModel showing whether the line is different (Context) compared to the other view and its linenumber (IndexLineNumber) - you can download the branch and compile/start the TextFileDemo to see how that currently works...

Where I cannot seem to get further is at the binding - I have tried to use a binding similar to one of your demo projects but I cannot seem to get it to work :-( - the code I am having looks like this:

                <range:RangeScrollbar Grid.Column="1" Width="30" Margin="3"
                                      VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
                                      ViewportSize="{Binding NumberOfTextLinesInViewPort}"
                                      Minimum="0"
                                      Maximum="{Binding DiffCtrl.MaxNumberOfLines}"
                                      Value="{Binding OverViewValue, Mode=OneWay,UpdateSourceTrigger=PropertyChanged}">
                    <range:RangeItemsControl
                        ItemsSource="{Binding DiffCtrl.ViewA.DocLineDiffs}"
                        >
                        <range:RangeItemsControl.ItemTemplate>
                            <DataTemplate>
                                <Ellipse Name="elp" Fill="DarkRed" Width="8" Height="8"
                                         range:RangePanel.Position="{Binding IndexLineNumber}"
                                         range:RangePanel.Alignment="Center"
                                         />
                                <DataTemplate.Triggers>
                                    <Trigger Property="ItemsControl.AlternationIndex" Value="0">
                                        <Setter TargetName="elp" Property="Fill" Value="DarkSalmon" />
                                    </Trigger>
                                </DataTemplate.Triggers>
                            </DataTemplate>
                        </range:RangeItemsControl.ItemTemplate>
                    </range:RangeItemsControl>
</range:RangeScrollbar>

MainWindow.xaml

...but there is no Elipse item showing up in the RangeScrollbar UI space - what am I doning wrong? Could you please help me to get this to work? I probably don't understand all sides of the RangeScrollbar and the RangeItemsControl but I hope you can point me into the right direction :-)

Ttxman commented 5 years ago

I think that in similar case I was using CollectionContainer Resources and setting them like this: <range:RangeItemsControl.ItemsSource> <StaticResource.../> </...> I don't remember the reason its been a while :)

It's is too complicated to use it that way, so I made some minor changes for you (ItemsSource on RangeScrollbar is now DependencyProperty). And remember to redo your changes to XAML...

Just pull the latest version, Now it should work if you do something like this:

xmlns:vms="clr-namespace:AehnlichViewModelsLib.ViewModels;assembly=AehnlichViewModelsLib"
.
.
.

<range:RangeScrollbar 
    Grid.Column="1" 
    Width="30" Margin="3"
    VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
    ViewportSize="{Binding NumberOfTextLinesInViewPort}"
    Minimum="0"   
    Maximum="{Binding DiffCtrl.MaxNumberOfLines}"
    Value="{Binding OverViewValue, Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"
    ItemsSource="{Binding DiffCtrl.ViewA.DocLineDiffs}"
    >
    <range:RangeScrollbar.Resources>
        <DataTemplate DataType="{x:Type vms:DiffLineViewModel}">
            <Ellipse Name="elp" Fill="DarkRed" Width="8" Height="8"
                range:RangePanel.Position="{Binding IndexLineNumber}"
                range:RangePanel.Alignment="Center"
                />
        </DataTemplate>
    </range:RangeScrollbar.Resources>
</range:RangeScrollbar>
Dirkster99 commented 5 years ago

Thanks for your quick tip/change - I did not know you can use a StaticResource on a normal property :-) So, I guess I learned another trick but my problem was exactly the ItemsSource not being a DP - I guess that missery is solved :-) and is working well in the latest commit:

There is one problem though where I cannot seem to get passed it and I am wondering if you might have experienced this before(?) - here is the workflow to verify my issue:

1) Scroll the left or right view down/up and the RangeScrollBar (now displayed on the far left side) will scroll its thumb as expected [OK].

2) Drag the thumb in the RangeScrollBar or click into its track and the left and right view change their display accordingly [OK].

3) Scroll the left or right view down/up as in 1) -> Issue: The thumb in the RangeScrollBar does not move anymore even though the bound OverViewValue property is still changed.

            <range:RangeScrollbar Grid.Column="0" Grid.Row="0" Grid.RowSpan="3" Width="30" Margin="3"
                                  Name="OverviewCtrl"
                                  VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
                                  ViewportSize="{Binding NumberOfTextLinesInViewPort}"
                                  Minimum="0"
                                  Maximum="{Binding DiffCtrl.MaxNumberOfLines}"
                                  Value="{Binding OverViewValue, Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"
                                  ValueChangedCommand="{Binding OverviewValueChangedCommand,Mode=OneWay}"
                                  ItemsSource="{Binding DiffCtrl.ViewA.DocLineDiffs}"
                                >
                <range:RangeScrollbar.ItemTemplate>
                    <DataTemplate DataType="{x:Type vms:DiffLineViewModel}">
                        <Rectangle Name="elp"
                                    Fill="{Binding Context,Converter={StaticResource DiffContextToColorPropConverter}}"
                                    Width="{Binding ElementName=OverviewCtrl,Path=ActualWidth}" Height="1"
                                    Visibility="{Binding Context,Converter={StaticResource DiffContextToVisibilityPropConverter}}"
                                    range:RangePanel.Position="{Binding IndexLineNumber}"
                                    range:RangePanel.Alignment="Begin"
                                    HorizontalAlignment="Stretch"
                                />
                    </DataTemplate>
                </range:RangeScrollbar.ItemTemplate>
            </range:RangeScrollbar>

I have tried different XAMLs including activating/deactivating properties but I cannot seem to find the cause of the thumb not being syncronized after 3) - would you have an idea what I could try?

Ttxman commented 5 years ago

Your situation looks like the binding is broken after the Value is set from RangeScrollBar I can't check the code right now but the issue could be that you are using one way binding to Value: Value="{Binding OverViewValue, Mode=OneWay,UpdateSourceTrigger=PropertyChanged}" So when Value is directly set, it is not propagated to binding Source (it bound one way FROM source) but directly replaced by fixed number. I believe that normal Scrollbar behave exactly same way.

Dirkster99 commented 5 years ago

Yes you are right the binding was broken just because I did not use the 2 way binding :-( - I am now using an internal double preview value to destinguish between the values set by the control and the value set by the viewmodel and got rid of the command (and that seemed to fix it):

        public double OverViewValue
        {
            get { return _OverViewValue; }
            set
            {
                if (Math.Abs(_OverViewValue - value) > 1)
                {
                    _OverViewValue = value;
                    NotifyPropertyChanged(() => OverViewValue);

                    // Value has been set by control and not by viewmodel -> Sync left/right views
                    if (_previewOverViewValue != _OverViewValue)
                    {
                        if (OverviewValueChangedCanExecute())
                           OverviewValueChanged(_OverViewValue);
                    }
                }
            }
        }
Ttxman commented 5 years ago

Sorry, but I find the NotifyPropertyChanged(() => OverViewValue); line disturbing :) There is CallerMemberNameAttribute and the nameof operator that make this so much better

Dirkster99 commented 5 years ago

I was not aware of these: https://stackoverflow.com/questions/36149863/how-to-write-a-viewmodelbase-in-mvvm

For the Diff overview I'd like to be able to click on a certain spot on the track and have the side-by-side views jump there immidiately rather than having to page/scroll towards it. For this I've tried to implement Mouse Listner on the track and the scrollbar but could not get past the normal Scrollbar behavior - would you know how I can override the normal click in track behavior and have the scrollbar jump to the position I clicked on?

I go this much but I cannot find an equivalent to the sv.ScrollToVerticalOffset(p.Y * factor); line:

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _PART_RangeOverlay = base.GetTemplateChild("PART_RangeOverlay") as RangeItemsControl;
            _PART_Track = base.GetTemplateChild("PART_Track") as Track;

            if (_PART_RangeOverlay == null)
                return;

            if (_PART_Track != null)
                this.PreviewMouseDown += _PART_RangeOverlay_PreviewMouseDown;

            // Are there any items specified for rendering in the contents property of this control?
            if (_iItems != null && _iItems.Count > 0)
            {
                // Copy the items and raise property changed for items collection
                foreach (var item in _iItems)
                    _PART_RangeOverlay.Items.Add(item);

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Items"));

                _iItems = null;
            }
        }

        private void _PART_RangeOverlay_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            Rectangle rect = Mouse.DirectlyOver as Rectangle;
            if (rect != null)
            {
                var sv = sender as ScrollBar;

                double trackHeight = _PART_Track.ActualHeight;
                double factor = _PART_Track.Thumb.ActualHeight / trackHeight;

                Point p = e.GetPosition(_PART_Track);
//                sv.ScrollToVerticalOffset(p.Y * factor);

                e.Handled = true;
            }
        }
Ttxman commented 5 years ago

I think just setting RangeScrollbar.Value to something along the trackHeight / p.Y or factor should work.

Dirkster99 commented 5 years ago

I've been thinking along the same lines and been trying the same code snippets before with no success - but your comment made me review this and look deeper and it turns out that we have to convert the percentage value from the trackHeight and mouse pointer Y position into the value range between Min and Max:

private void _PART_RangeOverlay_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
    if ((e.LeftButton == MouseButtonState.Pressed) == false)
        return;

    Rectangle rect = Mouse.DirectlyOver as Rectangle;
    if (rect != null)
    {
        var sv = sender as ScrollBar;

        // Find the percentage value where the mouse click occured on the track
        double trackHeight = _PART_Track.ActualHeight;
        Point p = e.GetPosition(_PART_Track);
        double factor = p.Y / _PART_Track.ActualHeight;

        // Convert the percentage value into the actual value scale
        double targetValue = Math.Abs(Maximum - Minimum) * factor;
        this.Value = Minimum + targetValue;

        e.Handled = true;
    }
}

I still have a bug in my app to fix because it works only if I click two times in a different position of the track but thats clearly an app problem as I already noticed it before - its just that have 3 controls (Overview, left view, and right view) which need to be snynchronized but preferable with recursive loops :-) which makes this one a bit extra difficult but I am sure I'll get that fixed next :-)

Thanks for your suggestion - this chat has been really useful for me as I learned a few things here and there.