I am trying to learn WPF on my own, and it is a bit of a struggle. I need to know how to set the value of an attached property via binding. The attached properties Grid.Row and Grid.RowSpan are the destination for the binding, not the source. Similar questions have been asked on StackOverflow, but they were asked and answered by people who really know WPF, or they involve complications like value converters. I haven't found an answer that is applicable and comprehensible to me.
In this case, I have a grid that represents a full day's schedule, and I want to add events to it. Each event will start at a particular grid row and span a number of rows, depending on the event's start time and duration. My understanding is that you have to use dependency properties as the source for bindings, so my event object, ActivityBlock
, has the StartIncrement
and DurationIncrements
dependency properties to describe its position on the grid.
That's it, that's all I want to do: make a UserControl position itself within a grid via bindings.
I believe my problem is most likely in my MainWindow XAML, setting up the bindings incorrectly on the grid. (Note that I create the grid rows programmatically in the constructor, so don't look for them in the XAML below).
When I run my application, the event is created, but it doesn't show up in the proper location on the grid, as if Grid.Row and Grid.RowSpan are never being updated. Is binding Grid.Row and Grid.RowSpan impossible? If not, what am I doing wrong?
Here is MainWindow.xaml, where my problem is most likely to be:
<Window x:Class="DayCalendar.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DayCalendar"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" Activated="Window_Activated"
>
<DockPanel LastChildFill="True">
<Rectangle Width="50" DockPanel.Dock="Left"/>
<Rectangle Width="50" DockPanel.Dock="Right"/>
<Grid x:Name="TheDay" Background="#EEE">
<ItemsControl ItemsSource="{Binding Path=Children}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Grid.Row" Value="{Binding Path=StartIncrement}" />
<Setter Property="Grid.RowSpan" Value="{Binding Path=DurationIncrements}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</Grid>
</DockPanel>
</Window>
Here is the code-behind file for the MainWindow:
using System;
using System.Windows;
using System.Windows.Controls;
using DayCalendar.MyControls;
namespace DayCalendar {
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
var rowDefs = TheDay.RowDefinitions;
rowDefs.Clear();
for ( int ix = 0; ix < ( 60 / ActivityBlock.MINUTES_PER_INCREMENT ) * 24; ix += 1 ) {
rowDefs.Add( new RowDefinition() );
}
}
private void Window_Activated( object sender, EventArgs e ) {
if ( m_firstActivation ) {
m_firstActivation = false;
var firstActivity = new ActivityBlock();
var today = DateTime.Now.Date;
var startTime = today.AddHours( 11.5 );
var durationSpan = new TimeSpan( 1, 0, 0 );
firstActivity.SetTimeSpan( startTime, durationSpan );
TheDay.Children.Add( firstActivity );
}
}
private bool m_firstActivation = true;
}
}
Here is the ActivityBlock code:
using System;
using System.Windows;
using System.Windows.Controls;
namespace DayCalendar.MyControls {
public partial class ActivityBlock : UserControl {
public int StartIncrement {
get { return ( int ) GetValue( StartIncrementProperty ); }
set { SetValue( StartIncrementProperty, value ); }
}
public int DurationIncrements {
get { return ( int ) GetValue( DurationIncrementsProperty ); }
set { SetValue( DurationIncrementsProperty, value ); }
}
public ActivityBlock() {
InitializeComponent();
}
public void SetTimeSpan( DateTime startTime, TimeSpan duration ) {
int startMinute = startTime.Hour * 60 + startTime.Minute;
int durationMinutes = ( int ) duration.TotalMinutes;
StartIncrement = startMinute / MINUTES_PER_INCREMENT;
DurationIncrements = Math.Max( 1, durationMinutes / MINUTES_PER_INCREMENT );
}
static ActivityBlock() {
var thisType = typeof( ActivityBlock );
var affectsArrangeAndMeasure = FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure;
int startIncrementDefault = 0;
StartIncrementProperty = DependencyProperty.Register(
nameof( StartIncrement ),
startIncrementDefault.GetType(),
thisType,
new FrameworkPropertyMetadata( startIncrementDefault, affectsArrangeAndMeasure )
);
int durationIncrementsDefault = 1;
DurationIncrementsProperty = DependencyProperty.Register(
nameof( DurationIncrements ),
durationIncrementsDefault.GetType(),
thisType,
new FrameworkPropertyMetadata( startIncrementDefault, affectsArrangeAndMeasure )
);
}
public const int MINUTES_PER_INCREMENT = 6; // 1/10th of an hour
static public readonly DependencyProperty StartIncrementProperty;
static public readonly DependencyProperty DurationIncrementsProperty;
}
}
The corresponding XAML isn't interesting, but I include it in case it is needed:
<UserControl x:Class="DayCalendar.MyControls.ActivityBlock"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:DayCalendar.MyControls"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Border BorderThickness="3" BorderBrush="Black" Background="Chartreuse">
<DockPanel LastChildFill="True">
<TextBlock TextWrapping="WrapWithOverflow">Event Description</TextBlock>
</DockPanel>
</Border>
</UserControl>