J4N
J4N

Reputation: 20697

How to bind Grid Row Height to a static constant of another class

In some class, I've a constant. The constant is used by some other ViewModel to compute placements inside of a canvas (and some other stuff).

In a view I need to use those constants for 2 RowDefinition Height.

<Grid Margin="0">
    <Grid.RowDefinitions>
        <RowDefinition Height="{x:Static local:SomeModel.PART_A_SIZE}"></RowDefinition>
        <RowDefinition Height="{x:Static local:SomeModel.PART_B_SIZE}"></RowDefinition>
    </Grid.RowDefinitions>
    <!-- ... -->
</Grid>

The issue is that the Height is a GridLength and does not convert from double.

I first tought of using a converter, but they can only be used inside a binding. I cannot use a resource since the value has to be usable outside of this control (outside of WPF actually).

So how can I achieve this without repeating the size at 2 places?

Upvotes: 0

Views: 428

Answers (3)

thatguy
thatguy

Reputation: 22079

A GridLength can have different sizing options like Pixel and Star. Consequently, depending on your use-case, there might be a custom conversion needed. Here a few examples how to achieve that.

Binding (with Custom Converter)

You can actually bind to a static property or field, by specifying it as Source (with or without converter).

<Grid Margin="0">
   <Grid.Resources>
      <local:DoubleToGridLengthConverter x:Key="DoubleToGridLengthConverter"/>
   </Grid.Resources>
   <Grid.RowDefinitions>
      <RowDefinition Height="{Binding Source={x:Static local:SomeModel.PART_A_SIZE}, Converter={StaticResource DoubleToGridLengthConverter}}"/>
      <RowDefinition Height="{Binding Source={x:Static local:SomeModel.PART_B_SIZE}, Converter={StaticResource DoubleToGridLengthConverter}}"/>
   </Grid.RowDefinitions>
   <!-- ... -->
</Grid>

Then you can use a custom converter (if you need it) to convert your double values to GridLength, e.g.:

public class DoubleToGridLengthConverter : IValueConverter
{
   public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
   {
      if (!(value is double doubleValue))
         return null;

      return new GridLength(doubleValue, GridUnitType.Star);
   }

   public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
   {
      throw new InvalidOperationException("This conversion is not supported.");
   }
}

I first tought of using a converter, but they can only be used inside a binding.

Indeed, value converters are used in bindings, but the conversion can be done in other ways, too, if you are not dealing with changing properties anyway or use constants only.

Markup Extension

A rather elegant way could also be to create a custom markup extension with a custom conversion.

public class CustomGridLengthExtension : StaticExtension
{
   public CustomGridLengthExtension()
   {
   }

   public CustomGridLengthExtension(string member) : base(member)
   {
   }

   public override object ProvideValue(IServiceProvider serviceProvider)
   {
      if(!(base.ProvideValue(serviceProvider) is double value))
         return null;

      return Convert(value);
   }

   private static GridLength Convert(double value)
   {
      return new GridLength(value, GridUnitType.Star);
   }
}

Of course you can extend this to parametrize the extension with the grid unit type or anything else.

<Grid Margin="0">
   <Grid.RowDefinitions>
      <RowDefinition Height="{local:CustomGridLength local:SomeModel.PART_A_SIZE}"/>
      <RowDefinition Height="{local:CustomGridLength local:SomeModel.PART_B_SIZE}"/>
   </Grid.RowDefinitions>
   <!-- ... -->
</Grid>

Object Data Provider

Just for completeness, since you are using a static value anyway, you could also define the corresponding GridLength pendants in XAML using an ObjectDataProvider if you do not need a conversion that changes the double values itself, but only the unit type (Auto, Star, Pixel). You cannot define GridLengths directly, as they require constructor parameters and do not expose any properties to set after creation.

<Grid Margin="0">
   <Grid.Resources>
      <ObjectDataProvider x:Key="PART_A_GRID_LENGTH"
                          ObjectType="{x:Type GridLength}">
         <ObjectDataProvider.ConstructorParameters>
            <x:Static MemberType="{x:Type local:SomeModel}" Member="PART_A_SIZE"/>
            <x:Static MemberType="{x:Type GridUnitType}" Member="Star"/>
         </ObjectDataProvider.ConstructorParameters>
      </ObjectDataProvider>
      <ObjectDataProvider x:Key="PART_B_GRID_LENGTH"
                          ObjectType="{x:Type GridLength}">
         <ObjectDataProvider.ConstructorParameters>
            <x:Static MemberType="{x:Type local:SomeModel}" Member="PART_B_SIZE"/>
            <x:Static MemberType="{x:Type GridUnitType}" Member="Star"/>
         </ObjectDataProvider.ConstructorParameters>
      </ObjectDataProvider>
   </Grid.Resources>
   <Grid.RowDefinitions>
      <RowDefinition Height="{Binding Source={StaticResource PART_A_GRID_LENGTH}}"/>
      <RowDefinition Height="{Binding Source={StaticResource PART_B_GRID_LENGTH}}"/>
   </Grid.RowDefinitions>

   <Border Grid.Row="0" Background="Red"/>
   <Border Grid.Row="1" Background="Blue"/>
   <!-- ... -->
</Grid>

Upvotes: 0

EldHasp
EldHasp

Reputation: 7908

<Grid Margin="0">
    <Grid.RowDefinitions>
        <RowDefinition Height="{Binding Path=(local:SomeModel.PART_A_SIZE)}"></RowDefinition>
        <RowDefinition Height="{Binding Path=(local:SomeModel.PART_B_SIZE)}"></RowDefinition>
    </Grid.RowDefinitions>
    ...
</Grid>

Auto-update of static property binding is also supported. To do this, you need to either implement the event of the same name with the endwith Changed. This is more typical for Forms, but works for WPF as well. Or a StaticPropertyChanged event with a property name.

public static double PART_A_SIZE 
{
    get => _part_a_size;
    set
    {
       if(!Equals(_part_a_size, value))
       {
           _part_a_size = value;
           PART_A_SIZEChanged?.Invoke(null, EventArgs.Empty);
        }
    }
}
private static double _part_a_size;
public static event EventHandler PART_A_SIZEChanged;
public static double PART_A_SIZE 
{
    get => _part_a_size;
    set
    {
       if(!Equals(_part_a_size, value))
       {
           _part_a_size = value;
           StaticPropertyChanged?.Invoke(null, new PropertyChangedEventArgs(nameof(PART_A_SIZE)));
        }
    }
}
private static double _part_a_size;
public static event EventHandler<PropertyChangedEventArgs> StaticPropertyChanged;

If you need bindings to Constants, then you need to pass them to the Source of the bindings.

<Grid Margin="0">
    <Grid.RowDefinitions>
        <RowDefinition Height="{Binding Source={x:Static local:SomeModel.PART_A_SIZE}}"></RowDefinition>
        <RowDefinition Height="{Binding Source={x:Static local:SomeModel.PART_B_SIZE}}"></RowDefinition>
    </Grid.RowDefinitions>
    ...
</Grid>

Upvotes: 0

Clemens
Clemens

Reputation: 128042

You could bind a constant

public class SomeModel
{
    public const double PART_A_SIZE = 100;
}

or a static field

public class SomeModel
{
    public static double PART_A_SIZE = 100;
}

like

<RowDefinition Height="{Binding Source={x:Static local:SomeModel.PART_A_SIZE}}"/>

and would thus benefit from built-in type conversion from double to GridLength.


Assuming you have a static property

public class SomeModel
{
    public static double PART_A_SIZE { get; } = 100;
}

you could also bind it like

<RowDefinition Height="{Binding Path=(local:SomeModel.PART_A_SIZE)}"/>

Upvotes: 2

Related Questions