R – the best way to display a grid of a large number of items in Silverlight

reportingsilverlightsilverlight-2.0visualizationxaml

I am currently working on a Silverlight2-based attendance register. I want to create a visualisation of attendance over time for students and classes, but am struggling to come up with a good way of doing it. The sort of thing I'm imagining is a grid with students on the vertical axis and date along the horizontal, with a symbol at the intersection of student and date indicating presence or absence. Ideally the method used to generate the visualisation would also be used to generate printed material, however this is not essential. (Silverlight has no built-in printing support, so it'd have to be SQL Server Reporting Services or similar.)

Here's a simple mock-up in Excel of the sort of data I need to display:
Mock-up chart

(Obviously with awesome Silverlight styling)

Here are my current thoughts on how to go about it:

  • ListBox with a Canvas ItemsPanel and data-bound positioned items. This would be the best in some ways because it requires the least amount of code, however I have not been able to do this nicely in Silverlight2 (The closest I've got is a ListBox with a Canvas per item and the item contents positioned within that). It would potentially be very slow with hundreds or thousands of items all needing to be instantiated.
  • DataGrid with dynamic columns. I havent tried this yet, but as there may be 100+ columns in some cases, it doesnt seem to be a nice solution. Again I'd be concerned about performance with a large number of items.
  • Server-side image (PNG) generation. This would solve the printing problem because both the Silverlight client and Reporting Services could reference the same image, however this would preclude any sort of interactivity in the Silverlight client. Image resolution would also be an issue for print quality. The load when displaying thousands of items would be pushed to the server, so the speed of the client would not be affected, aside from the initial delay in generating the image on the server.
  • Silverlight Custom Control. I could build a custom control that would place the text/symbols on a canvas. This would effectively be a scatter-chart. This wouldnt solve the printing problem. Performance would likely be better than a ListBox due to lower overhead, however any databinding, item selection etc would have to be coded manually.
  • Use a 3rd-party Scatterplot Chart. This could be relatively simple, but would depend on the features of the chart library. Another solution would have to be found for printing purposes.
  • Server-side generation of SVG or XAML. Similar to image generation, but instead generates vector data for display or printing. SVG would be best for printing and export, although XPS documents are based on XAML, so that could work. Using SVG in Silverlight would require conversion to XAML. There are offline tools to do this, but no function to convert or render SVG within Silverlight.
  • Use large amounts of fixed-width text. We have some old reports here that do this kind of thing by generating strings like ......x...oo.ox...x.... which are then displayed in a fixed-width font. This solution makes my eyes bleed because it seems like a throw-back to the green-screen terminal days, especially when Silverlight is vector-based.

Basically every train of thought leads me towards writing my own fully-featured Silverlight/Reporting engine, which is way beyond the scope of what I'm trying to do. Plus I dont really want to leave a future maintainer with some awful custom-built hacky display & reporting system. (I dont want to end up on TheDailyWTF!)

This is the sort of visualisation that Silverlight is made for – I just cant decide where to direct my efforts.

Best Answer

I can think only a simple two level ItemsControl(ListBox) solution for this. And the inner visual element can be a Styled CheckBox to look like 'O' or 'X' here is the sample I just made for you . Of course you do need to a DateHeader collection at the top aligned with the checkboxes.

alt text http://img339.imageshack.us/img339/8695/grid.jpg

XAML

    <UserControl.Resources>
    <DataTemplate x:Key="CellTemplate">
        <Grid Width="25" Height="25">
            <CheckBox IsChecked="{Binding IsPresent}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Grid>
    </DataTemplate>
    <DataTemplate x:Key="RowTemplate">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="80"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <TextBlock Margin="4,0,0,0" Text="{Binding Name}" TextWrapping="Wrap"/>
            <ItemsControl ItemsSource="{Binding WorkingDays}" HorizontalAlignment="Left" VerticalAlignment="Top" ItemTemplate="{StaticResource CellTemplate}" Grid.Column="1" >
                <ItemsControl.ItemsPanel>   
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Horizontal"/>      
                    </ItemsPanelTemplate>               
                </ItemsControl.ItemsPanel>
            </ItemsControl>
        </Grid>
    </DataTemplate>
</UserControl.Resources>
<ItemsControl x:Name="lstWorkingDaysMain" ItemsSource="{Binding}" ItemTemplate="{StaticResource RowTemplate}" >
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Vertical"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl> 

C#

public class Student
{
    public Student()
    {
        WorkingDays = new List<WorkingDay>();
    }

    public string Name { get; set; }
    public List<WorkingDay> WorkingDays { get; set; }
}

public class WorkingDay
{
    public bool IsPresent{get; set;}
    public DateTime Date { get; set; }
}

And the test data population at the code behind xaml.cs

 List<Student> students = new List<Student>();

        Student student = new Student() { Name = "Aaaaaa" };
        student.WorkingDays.Add(new WorkingDay() { Date=new DateTime(2009,5,5), IsPresent=true} );
        student.WorkingDays.Add(new WorkingDay() { Date=new DateTime(2009,5,6), IsPresent=true} );
        student.WorkingDays.Add(new WorkingDay() { Date=new DateTime(2009,5,7), IsPresent=true} );
        student.WorkingDays.Add(new WorkingDay() { Date=new DateTime(2009,5,8), IsPresent=true} );
        student.WorkingDays.Add(new WorkingDay() { Date=new DateTime(2009,5,9), IsPresent=true} );
        student.WorkingDays.Add(new WorkingDay() { Date=new DateTime(2009,5,10), IsPresent=true} );
        student.WorkingDays.Add(new WorkingDay() { Date=new DateTime(2009,5,11), IsPresent=true} );
        student.WorkingDays.Add(new WorkingDay() { Date=new DateTime(2009,5,12), IsPresent=true} );
        students.Add(student);

        student = new Student() { Name = "Bbbbbb" };
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 5), IsPresent = true });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 6), IsPresent = true });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 7), IsPresent = true });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 8), IsPresent = false });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 9), IsPresent = false });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 10), IsPresent = true });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 11), IsPresent = true });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 12), IsPresent = true });
        students.Add(student);


        student = new Student() { Name = "Cccccc" };
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 5), IsPresent = true });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 6), IsPresent = true });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 7), IsPresent = true });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 8), IsPresent = false });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 9), IsPresent = false });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 10), IsPresent = true });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 11), IsPresent = true });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 12), IsPresent = false });
        students.Add(student);


        student = new Student() { Name = "Dddddd" };
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 5), IsPresent = false });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 6), IsPresent = true });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 7), IsPresent = true });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 8), IsPresent = true });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 9), IsPresent = false });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 10), IsPresent = true });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 11), IsPresent = true });
        student.WorkingDays.Add(new WorkingDay() { Date = new DateTime(2009, 5, 12), IsPresent = true });
        students.Add(student);

        this.DataContext = students;
Related Topic