时间线中可调整大小的片段:将片段大小绑定到视图模型

问题描述

我正在尝试实现一个用户控件,该控件代表一个时间轴(如在视频编辑器中),其中包含一个片段,该片段的开始和结束标记可由用户拖动。

我在我的视图模型中代表一个这样的段:

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

并尝试使用带有网格拆分器的网格来实现视图,如下所示:

<Grid ColumnDeFinitions="Auto,3,Auto,*" 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>

这有效,因为我可以在视觉上稍微调整段的大小:

timeline drag example gif

但是,我正在努力将大小更改返回到视图模型中,您也可以在未更改大小的红色区域中看到该更改。我找不到检索 SpacerLeftSpacerSegment 控件的宽度变化的方法

我尝试的是删除专用的 Width 属性,而是绑定网格的 ColumnDeFinitions。我将此属性添加到我的视图模型中:

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

并将视图 XAML 更改为:

<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>

但是由于一个对我来说没有多大意义的原因,这无法编译:

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
  ...

解决方法

第一个问题是试图直接绑定到 Grid 的 ColumnDefinitions。相反,使用长定义形式并直接绑定到每列的宽度是要走的路:

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

还需要明确指定 Mode=TwoWay,因为默认情况下 ColumnDefinition 的宽度绑定不是双向的。

接下来,由于 ColumnDefinitionWidth 属性实际上属于 GridLength 类型而不是 double,因此需要 IValueConverter,如{{3 }}。

此时网格的 XAML 应如下所示

<Grid HorizontalAlignment="Stretch">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="{Binding From,Mode=TwoWay,Converter={StaticResource GridLengthConverter}}" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="{Binding Duration,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>

此外,要修复左网格拆分器移动选择而不是将其扩展到左侧,Moment 视图模型需要使用 FromTo 属性来表示段FromDuration 属性,并且 Duration 属性派生自其他两个属性。这需要手动连接一些属性更改事件,而不是依赖于 [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;
}

通过这些更改,结果如下所示:

here