2

I am trying to implement a user control that represents a timeline (like in a video editor) with a segment whose start- and end-marker can be dragged by the user.

I am representing a segment like this in my view model:

public class Moment : ViewModelBase
{
    [Reactive] public double From { get; set; }
    [Reactive] public double Duration { get; set; }
}

And attempted to implement the view using a grid with grid splitters like this:

<Grid ColumnDefinitions="Auto,3,Auto,3,*" HorizontalAlignment="Stretch">
    <Panel Grid.Column="0" Name="SpacerLeft"
           Width="{Binding From}" />
    <GridSplitter Grid.Column="1" Background="cyan" />
    <Rectangle Grid.Column="2" Name="SpacerSegment"
               HorizontalAlignment="Stretch" Fill="red" Height="40"
               Width="{Binding Duration}">
    </Rectangle>
    <GridSplitter Grid.Column="3" Background="cyan" />
    <Panel Grid.Column="4" Name="SpacerRight"/>
</Grid>

This works as in I can somewhat visually resize the segment:

timeline drag example gif

However, I am struggling to get the size changes back into the view model, which you can also see in the red area not changing size. I couldn't find a way to retrieve the width changes of the SpacerLeft or SpacerSegment controls.

What I tried instead was removing the dedicated Width properties and instead binding the grid's ColumnDefinitions. I added this property to my view model:

public ColumnDefinitions ColumnDefinitions
{
    get => ColumnDefinitions.Parse($"{From},3,{Duration},3,*");
    set
    {
        From = value[0].ActualWidth;
        Duration = value[2].ActualWidth;
    }
}

and changed the view XAML to this:

<Grid ColumnDefinitions="{Binding ColumnDefinitions}" HorizontalAlignment="Stretch">
    <Panel Grid.Column="0" Name="SpacerLeft" />
    <GridSplitter Grid.Column="1" Background="cyan" />
    <Rectangle Grid.Column="2" Name="SpacerSegment"
               HorizontalAlignment="Stretch" Fill="red" Height="40">
    </Rectangle>
    <GridSplitter Grid.Column="3" Background="cyan" />
    <Panel Grid.Column="4" Name="SpacerRight"/>
</Grid>

But this fails to compile for a reason that unfortunately doesn't make much sense to me:

InvalidCastException: Unable to cast object of type 'Avalonia.Data.Binding' to type 'Avalonia.Controls.ColumnDefinition'. System.InvalidCastException: Unable to cast object of type 'Avalonia.Data.Binding' to type 'Avalonia.Controls.ColumnDefinition'.
  at Avalonia.Collections.AvaloniaList`1.System.Collections.IList.Add(Object value) in /_/src/Avalonia.Base/Collections/AvaloniaList.cs:line 520
  at Builder_1ee6d795025442edb279bcc7110e88eb_avares://AvaloniaOutseekClient/Views/MomentsSourceView.axaml.XamlClosure_2.Build(IServiceProvider )
  at Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers.<>c__DisplayClass0_0.<DeferredTransformationFactoryV1>b__0(IServiceProvider sp) in /_/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs:line 28
  at Avalonia.Markup.Xaml.Templates.TemplateContent.Load(Object templateContent) in /_/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs:line 17
  at Avalonia.Markup.Xaml.Templates.DataTemplate.Build(Object data, IControl existing) in /_/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs:line 33
  at Avalonia.Controls.Presenters.ContentPresenter.CreateChild() in /_/src/Avalonia.Controls/Presenters/ContentPresenter.cs:line 356
  ...
Felk
  • 7,720
  • 2
  • 35
  • 65

1 Answers1

2

The first problem is trying to directly Bind to Grid's ColumnDefinitions. Instead, using the long definition form and binding to each column's width directly is the way to go:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="{Binding ...}" />
        <ColumnDefinition Width="Auto" />
        <!-- ... -->
    </Grid.ColumnDefinitions>
</Grid>

It also is necessary to explicitly specify Mode=TwoWay, as bindings to a ColumnDefinition's width are not two-way by default.

Next, because a ColumnDefinition's Width property is actually of type GridLength and not double, a IValueConverter is required as described here.

At this point the grid's XAML should look like this

<Grid HorizontalAlignment="Stretch">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="{Binding From, Mode=TwoWay, Converter={StaticResource GridLengthConverter}}" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="{Binding Duration, Mode=TwoWay, Converter={StaticResource GridLengthConverter}}" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <Panel Grid.Column="0" Name="SpacerLeft" />
    <GridSplitter Grid.Column="1" Background="cyan" />
    <Rectangle Grid.Column="2" Name="SpacerSegment"
               HorizontalAlignment="Stretch" Fill="red" Height="40">
    </Rectangle>
    <GridSplitter Grid.Column="3" Background="cyan" />
    <Panel Grid.Column="4" Name="SpacerRight"/>
</Grid>

Furthermore, to fix the left grid splitter moving the selection instead of extending it to the left, the Moment view model needs to represent the segment with a From and To property instead of a From and Duration property, and have the Duration property derive from the other two instead. This requires hooking up some property change events by hand instead of relying on [Reactive]:

private double _from;
private double _to;

public double From
{
    get => _from;
    set
    {
        if (Equals(_from, value)) 
            return;
        this.RaisePropertyChanging(nameof(Duration));
        this.RaisePropertyChanging();
        _from = value;
        this.RaisePropertyChanged();
        this.RaisePropertyChanged(nameof(Duration));
    }
}

public double To
{
    get => _to;
    set
    {
        if (Equals(_to, value)) 
            return;
        this.RaisePropertyChanging(nameof(Duration));
        this.RaisePropertyChanging();
        _to = value;
        this.RaisePropertyChanged();
        this.RaisePropertyChanged(nameof(Duration));
    }
}

public double Duration
{
    get => To - From;
    set => To = From + value;
}

With these changes the result looks like this:

segment slider demo

Felk
  • 7,720
  • 2
  • 35
  • 65