1

I'm creating a UWP application that has a custom control.

I have a model that looks like this:

public enum SpanType {
    Normal,
    Suspend,
    Resume,
}

public struct Span {
    public SpanType Type;
    public int StackIndex;
    public string Category;
    public string Name;
    public UInt64 ThreadId;
    public UInt64 SpanId;
    public UInt64 StartTime;
    public UInt64 Duration;
}

public struct ThreadSpans {
    public UInt64 ThreadId;
    public List<Span> Spans;
    public int RowCount;
}

And the custom control:

public sealed partial class ThreadSpanControl : UserControl {
    public ThreadSpanControl() {
        DataContext = this;

        this.InitializeComponent();
    }

    public List<ThreadSpans> Threads {
        get { return (List<ThreadSpans>)GetValue(ThreadsProperty); }
        set { SetValue(ThreadsProperty, value); }
    }
    public static readonly DependencyProperty ThreadsProperty =
        DependencyProperty.Register("Threads", typeof(List<ThreadSpans>), typeof(ThreadSpanControl), null);

public float RowHeight {
        get { return (float)GetValue(RowHeightProperty); }
        set { SetValue(RowHeightProperty, value); }
    }
    public static readonly DependencyProperty RowHeightProperty =
        DependencyProperty.Register("RowHeight", typeof(float), typeof(ThreadSpanControl), new PropertyMetadata(20.0f));
}

My main window uses it like so:

<controls:ThreadSpanControl Threads="{x:Bind ViewModel.Threads,Mode=OneWay}" />

What I'd like to do is to be able to use the RowHeight property within the ItemsControl of my Custom control. Something like:

<UserControl
    x:Class="Fibofiler.Shared.Controls.ThreadSpanControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Fibofiler.Shared.Controls"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:controls="using:Fibofiler.Shared.Controls"
    xmlns:models="using:Fibofiler.Shared.Models"
    xmlns:skia="using:SkiaSharp.Views.UWP"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <ItemsControl Grid.Column="0" ItemsSource="{x:Bind Threads,Mode=OneWay}">
            <ItemsControl.ItemTemplate>
                <DataTemplate x:DataType="models:ThreadSpans">
                    <TextBlock Text="{x:Bind ThreadId,Mode=OneWay}" Height="{x:Bind controls:Converters.CalcHeight(RowCount, this.RowHeight)}" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
        <skia:SKXamlCanvas Grid.Column="1" PaintSurface="SKXamlCanvas_OnPaintSurface" />
    </Grid>
</UserControl>

But, that's not possible. The scope of the template of the ItemsControl is only the item itself. So as far as I can tell, there's no way to access to parent's properties. Is this correct?

So, to get around this, I thought perhaps I could make a new dependency property called RowHeaders, which just directly calculates the things I need. So then I can bind the ItemsSource to this instead, and have all the information I need in the template.

public struct ThreadHeader {
    public UInt64 ThreadId;
    public double Height;
};

public sealed partial class ThreadSpanControl : UserControl {
    ...

    public List<ThreadHeader> ThreadHeaders {
        get { return (List<ThreadHeader>)GetValue(ThreadHeadersProperty); }
    }
    private static readonly DependencyProperty ThreadHeadersProperty =
        DependencyProperty.Register("ThreadHeaders", typeof(List<ThreadHeader>), typeof(ThreadSpanControl), null);
    
    ...
}

The issue is this should be a "calculated / derived" property. That is, it directly depends on the Threads / RowHeight properties. And I'm not sure how to do that with DependencyProperties. All the online information say that the "plain" Property wrapper can't have any custom logic, because during runtime, the code won't even be used.

How do I have a "calculated / derived" DependencyProperty whose value is calculated from other properties?

The other option I was thinking of was to implement INotifyPropertyChanged, and use that. However, all the places I searched said, "Never use INPC in a control. Always use use DependencyProperties. They're more efficient". Which is fine to say, but as far as I can tell, DependencyProperties are way less flexible. They can only be simple get/set with zero dependencies on others.

Thoughts? Am I just going at this the wrong way? Thus running into issues? Or is there more functionality to custom controls / DependencyProperties that I'm missing?

Additional Context

Application view

My custom context is to render a ton (20000 to 200000) of rectangles representing timespans. I initially did this with nested ItemControls, and Rectangles, but the performance was terrible due to the massive overhead of a Control per rectangle. So, instead, I'm using a SkiaSharp canvas to render all the rectangles for me. The data is all static after load, so the loss of features per rectangle is fine.

The problem I'm trying to solve here is how to line up the labels with the corresponding rows. In the picture above, you can see that they're just one after the other. From the data, I know the max number of rows per thread, and I define the row height in pixels. I just don't know the best way to set this per TextBlock for the labels.

Update 1

Following @Knoop 's suggestion, I tried naming the custom control, so I could access it from the template, but that didn't work.

<UserControl
    x:Class="Fibofiler.Shared.Controls.ThreadSpanControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Fibofiler.Shared.Controls"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:controls="using:Fibofiler.Shared.Controls"
    xmlns:models="using:Fibofiler.Shared.Models"
    xmlns:skia="using:SkiaSharp.Views.UWP"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400"
    x:Name="thisControl">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <ItemsControl Grid.Column="0" ItemsSource="{x:Bind Threads,Mode=OneWay}">
            <ItemsControl.ItemTemplate>
                <DataTemplate x:DataType="models:ThreadSpans">
                    <TextBlock Text="{x:Bind ThreadId,Mode=OneWay}" Height="{x:Bind controls:Converters.CalculateHeight(RowCount, thisControl.RowHeight)}" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
        <skia:SKXamlCanvas Grid.Column="1" PaintSurface="SKXamlCanvas_OnPaintSurface" />
    </Grid>
</UserControl>

Compile error

RichieSams
  • 131
  • 4
  • It's unclear to me what exactly you're trying to achieve so it's hard to answer. However this question "So as far as I can tell, there's no way to access to parent's properties. Is this correct?" is not correct. You can `x:Name` your `UserControl` (or `Grid` if that's the one you want) and reference that inside the binding in the template, gaining access to the properties of the parent control. On mobile atm so hard to look it up but I've answered questions with this construct before. –  Mar 27 '21 at 15:54
  • Thanks for the reply. I would like to have a ListControl of items, where their `Height` is calculated based on: the `RowCount` variable from the item, and the `RowHeight` variable from the control. I'll try the x:Name method. Thanks! – RichieSams Mar 27 '21 at 15:59
  • Hmm, that didn't seem to work. See update – RichieSams Mar 27 '21 at 17:04
  • 1
    You've only named it, but you're not referencing it in the binding. Maybe this might help: https://social.msdn.microsoft.com/Forums/en-US/51ca22a7-efbb-4577-9379-c71ac0236fa7/uwpxamlbinding-to-parent-properties?forum=wpdevelop –  Mar 27 '21 at 17:09
  • How do I reference it though, when using x:Bind (instead of Binding). Since you can only call functions with x:Bind. https://stackoverflow.com/a/32851493/1757179 I can't use Binding with a ValueConverter, because UWP doesn't support MultiValueConverters. – RichieSams Mar 27 '21 at 17:51

1 Answers1

0

You could try to get the value of RowHeight property in the Loading event handler of the ItemsControl and save the value into a global variable for later access in a Converter.

Here I create a Converter as an example:

Add a global variable to save the value of RowHeight property:

public struct ThreadHeader
{
    public static float RowHeight;  //Global variable to save the value of RowHeight property
};

Get the value of RowHeight property in the Loading event handler:

<ItemsControl Grid.Column="0" ItemsSource="{x:Bind Threads,Mode=OneWay}" 
    Loading="ItemsControl_Loading">
    ……
</ItemsControl>

private void ItemsControl_Loading(FrameworkElement sender, object args)
{
    ThreadHeader.RowHeight = RowHeight;
}

Add a converter to do the calculation:

public class CalculationConverter : IValueConverter
{
    // This converts the DateTime object to the string to display.
    public object Convert(object value, Type targetType,
        object parameter, string language)
    {
        var threadSpans = (ThreadSpans)value;
        var res = threadSpans.RowCount*1.0 * ThreadHeader.RowHeight;
        return res;
    }

    // No need to implement converting back on a one-way binding 
    public object ConvertBack(object value, Type targetType,
        object parameter, string language)
    {
        throw new NotImplementedException();
    }
}
YanGu
  • 3,006
  • 1
  • 3
  • 7