Derived Property Data Binding in Silverlight

For our Silverlight-based Schedule Proofing application at work, we have a special requirement for Summer Session, where they have course ‘blocks’, or a set of predefined dates beyond just ‘full term’ and ‘whatever’. This required a few interesting blocks, but mostly, it required some interesting tweaks related to data binding, that unfortunately, I had to do in code.

The relevant XAML looks like this:

<my:DataGrid x:Name="Selector"
            AutoGenerateColumns="False"
            HeadersVisibility="Column"
            GridLinesVisibility="None"
            IsReadOnly="True"
            Visibility="Visible"
            FontSize="11"
            RowDetailsVisibilityMode="VisibleWhenSelected">
    <my:DataGrid.Columns>
        <my:DataGridTemplateColumn Header="Start Date">
            <my:DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding StartDate}" Margin="5,4,5,4"/>
                </DataTemplate>
            </my:DataGridTemplateColumn.CellTemplate>
            <my:DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <mye:DatePicker SelectedDate="{Binding StartDate, Mode=TwoWay}" />
                </DataTemplate>
            </my:DataGridTemplateColumn.CellEditingTemplate>
        </my:DataGridTemplateColumn>
        <my:DataGridTemplateColumn Header="End Date">
            <my:DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding EndDate}" Margin="5,4,5,4"/>
                </DataTemplate>
            </my:DataGridTemplateColumn.CellTemplate>
            <my:DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <mye:DatePicker SelectedDate="{Binding EndDate, Mode=TwoWay}" />
                </DataTemplate>
            </my:DataGridTemplateColumn.CellEditingTemplate>
        </my:DataGridTemplateColumn>
            <my:DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding BlockName}" Margin="5,4,5,4"/>
                </DataTemplate>
            </my:DataGridTemplateColumn.CellTemplate>
            <my:DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <ComboBox ItemsSource="{Binding TermInfo.Blocks}" DisplayMemberPath="Name" SelectedItem="{Binding SelectedBlock, Mode=TwoWay}" />
                </DataTemplate>
            </my:DataGridTemplateColumn.CellEditingTemplate>
        </my:DataGridTemplateColumn>
    </my:DataGrid.Columns>
</my:DataGrid>

The relevant portions of the data structure looks basically like this:

class SectionData
{
    private DateTime startDate;
    private DateTime endDate;
    private YearTerms termInfo;

    public YearTerms TermInfo
    {
        get
        {
            return termInfo;
        }
    }

    public DateTime StartDate {
        get { return startDate; }
        set
        {
            startDate = value;
            NotifyPropertyChanged("StartDate");
        }
    }
    public DateTime EndDate {
        get { return endDate; }
        set
        {
            endDate = value;
            NotifyPropertyChanged("EndDate");
        }
    }

    public SummerSessionBlock SelectedBlock
    {
        get
        {
            if (termInfo.Blocks == null) return null;

            var b = termInfo.Blocks.Where(q => q.Begin == startDate)
                .Where(q => q.End == endDate)
                .SingleOrDefault();
            return b ?? termInfo.Blocks.Where(q => q.Name == "Custom").Single();
        }
        set
        {
            var b = termInfo.Blocks.Where(q => q.Begin == startDate)
                .Where(q => q.End == endDate)
                .Any();
            if ((value.Name == "Custom" && b) || value.Name != "Custom")
            {
                startDate = value.Begin;
                endDate = value.End;
            }
            NotifyPropertyChanged("Block");
            NotifyPropertyChanged("StartDate");
        }
    }

    public string BlockName
    {
        get
        {
            return SelectedBlock != null ? SelectedBlock.Name : string.Empty;
        }
    }
}

public class YearTerms
{
    public ObservableCollection Blocks { get; set; }
}

public class SummerSessionBlock
{
    public int Year { get; set; }
    public int Term { get; set; }
    public string Name { get; set; }
    public DateTime Begin { get; set; }
    public DateTime End { get; set; }
}

Alright, that’s a lot of code, and most of it, I’m not really going to address here, since I’m assuming a basic understanding of Data Binding and Dependency Properties. Basically, all those NotifyPropertyChanged calls ensure that the UI gets updated. Also, the YearTerms.Blocks property will, in my case, be NULL for all terms that aren’t a summer session.

Now, since blocks don’t mean anything outside of Summer Session, I don’t want that column to be visible outside of that case. This isn’t a big problem, since all the sections which will be viewed on one instance of this data grid will, by definition, be from the same term, however, attempts to use XAML DataBinding failed with XAML parsing errors.

    <my:DataGridTemplateColumn Header="Block"  Visibility="{Binding BlockName, Converter={StaticResource VVC}}">

The VVC StaticResource just sets the Visibility to Hidden if BlockName is the empty string, Visible otherwise. As I said, this excepts with an obscure XAML parsing error. Unfortunately, things that can be quite simple in XAML can be a real pain in code when using Silverlight or (presumably) WCF. Partially because of how you have to identify the column. The solution is fairly simple, when the DataContext of the DataGrid is modified, I simply determine what the visibility should be and call a function that sets the visibilty.

public void SetColumnVisibility(string columnHeader, Visibility visible)
{
    try {
        Selector.Columns.Where(c => c.Header.ToString() == columnHeader)
            .Single().Visibility = visible;
    } catch (Exception e) {
        throw new ArgumentException("Column '" + columnHeader + "' does not exist.");
    }
}

Have I mentioned yet that I really love LINQ? This code is all wrapped up in a custom control, but I’m debating converting this function to an extension method on the DataGrid, since nothing quite like it is offered. This function is called from the web service callback responsible for setting the data context on this control, and it ensures that users only see this column when it makes sense to. Would have been nice to data bind this, but in reality, the binding works because I know that the data which determines the column visibility is technically shared among all members of the data, but the Silverlight Runtime doesn’t have any way of knowing that for certain.

More annoying is the second problem. The Date fields should not be modifyable directly by the user unless they’ve selected a special ‘custom’ option in the Blocks list. The ‘custom’ option is added at runtime, and is defined with start and end dates being just within the boundaries of the full term option. Which involves setting the IsReadOnly flag on the Columns for Start Date and End Date. Again, the Data Binding fails with a completely unhelpful XAML parsing error, and in this case, the data that I’m binding against is unique for the row, and doesn’t effect the rows around it, so I was kind of at a loss.

The problem with IsReadOnly on the DataGridColumn is that it effects all rows, not really what I want. So, really it’s out. But I don’t see a way to bind the IsEnabled flag on the Cell using a data binding (I’m fairly new to Silverlight, so this is likely my failing). So, to code it is. The problem I found was that, Silverlight doesn’t make it really easy to access the individual cells for a datagrid. You can get access to columns easily, and rows through a few events, but to access the cell….that’s non-trivial.

private void SetDateEditability(DataGridRow dataGridRow)
{
    var msi = dataGridRow.DataContext as MasterSectionInfo;

    // The fields should be editable if either of these are true, but not otherwise
    bool editable = msi.BlockName == "Custom" || msi.BlockName == string.Empty;
    // Should only except if Columns can't be found
    try
    {
        // I need these columns with these two names.
        var e = Selector.Columns.Where(c => c.Header.ToString() == "Start Date" || c.Header.ToString() == "End Date").Select(c => c);
        // For each column in the LINQ query above
        foreach (var column in e)
        {
            // Get the Cell Content for the argument row
            var el = column.GetCellContent(dataGridRow);
            // Get the cell itself, and set it's IsEnabled flag.
            (el.Parent as DataGridCell).IsEnabled = editable;
        }
    }
    catch (Exception) { }
}

Now, the question is, when does this method need to be called? It needs to be called anytime the selection for the Block column changes, which is accomplished through the CellEditEnded event on the data grid. For my purposes, I check that the edited cell is in the Block column, to save just a bit of time, but you can always decide how necessary that is in your own application. However, this isn’t sufficient, since it doesn’t effect the rows as they load, so you’ll also need to add a LoadingRow event handler which calls into this method for each row as it loads.

So, there you have it. How I created a virtual property driven by two backing properties, and tied the editability of the backing properties to the selection. I’ll be the first to admit, it’s not the prettiest looking solution in the world, and I’m sure it could be done better, but I was under a deadline, and really, this works pretty cleanly, for a solution derived from a Silverlight newbie.