ItemsControl (virtualize football drive information)
I’m trying to emulate an NFL drive chart to visualize some drive data from a game. An example of what I’m roughly trying to emulate can be seen here, courtesy of nfl.com. I’m trying to achieve the following:
- Display the playing field
- Draw the data on top of the playing field
- When I mouse-over something, then I want to fire off an event (e.g. ToolTip)
http://www.nfl.com/gamecenter/2011100909/2011/REG5/jets@patriots#menu=drivechart
Here is where I’m at right now, first of all the xaml:
<StackPanel x:Name="myStackPanel" Orientation="Vertical">
<StackPanel.Background>
<ImageBrush ImageSource="{StaticResource PlayingField}" Stretch="Fill" />
</StackPanel.Background>
<ItemsControl Background="AliceBlue" Opacity="0.5"
ItemsSource="{Binding Path=DriveDetails}"
ItemTemplate="{StaticResource myDataTemplate}"
ItemsPanel="{StaticResource myItemsPanelTemplate}" />
</StackPanel>
The result of which being:
(Sorry, as I'm a newbie I can't add the screenshot!)
I will do my best to describe what I'm seeing, I'm sure most of you will have this figured out after reading the xaml. Basically I see a football playing field in the background and, in the top left corner - my ItemsControl control.
The StackPanel is self-explanatory, I now that I’m going to have problems later with the image with resizing as one yard does not represent one pixel but I’ll be leaving that for another day, my main concern is how I can visualize the data. Also, the drive visualization should not start in the top left corner :D So dealing purely with the ItemsControl I’m left with this:
(Sorry, as I'm a newbie I can't add the screenshot!)
Once again, I will attempt to describe the image. It is a closeup of the aforementioned ItemsControl control, with no background (football playing field) image.
Note: The ItemsControl Background has been set to AliceBlue so that it’s easier to see, it will be Transparent once I have this working.
The ItemsSource is bound to a list of the “Play” class:
public class Play
{
public int Quarter { get; set; }
public int Result { get; set; }
public PlayType TypeOfPlay { get; set; }
public double WidthOfPlay { get; set; }
}
PlayType is an enum:
public enum PlayType
{
Run,
Pass,
Penalty,
Punt,
FieldGoal
}
The list gets some data… (sample data, I know that a 1st down has been gotten lol)
List<Play> SamplePlays = new List<Play>()
{
new Play { Quarter = 1, Result = 5, TypeOfPlay = PlayType.Penalty, WidthOfPlay = 30 },
new Play { Quarter = 1, Result = 5, TypeOfPlay = PlayType.Run, WidthOfPlay = 30 },
new Play { Quarter = 1, Result = 5, TypeOfPlay = PlayType.Pass, WidthOfPlay = 30 },
new Play { Quarter = 1, Result = 40, TypeOfPlay = PlayType.Punt, WidthOfPlay = 240 }
};
… and is passed on with the help of a transfer parameter class to the ViewModel…
ucHomeInstance = ucDriveChartInstance;
((ucDriveChart)ucHomeInstance).setViewModel(new ucDriveChartVM(new ucDriveChartTP()
{ Plays = SamplePlays }));
… where it is assigned to the DriveDetails property
public class ucDriveChartVM : ViewModelBase
{
public List<Play> DriveDetails { get; set; }
public ucDriveChartVM(ucDriveChartTP transferParameter)
{
this.DriveDetails = transferParameter.Plays;
}
}
Here is the xaml for the ItemsPanel:
<ItemsPanelTemplate x:Key="myItemsPanelTemplate">
<StackPanel Orientation="Horizontal" IsItemsHost="True" />
</ItemsPanelTemplate>
Here is the xaml for the ItemTemplate – please note that I’m binding to WidthOfPlay as one yard is roughly 6 units on the background graphic, once I get the graphic and resizing sorted out, then I’ll bind to the actual Result property:
<DataTemplate x:Key="myDataTemplate">
<StackPanel Orientation="Horizontal" Width="{Binding WidthOfPlay}"
SnapsToDevicePixels="True" Style="{StaticResource onMouseOver}">
<Border Padding="0,5,0,5">
<Grid>
<StackPanel Style="{StaticResource onMouseOver}">
<Rectangle Width="{Binding开发者_开发问答 WidthOfPlay}" Height="10"
Fill="{Binding TypeOfPlay,
Converter={StaticResource PlayTypeToColourConverter}}" />
</StackPanel>
</Grid>
</Border>
</StackPanel>
</DataTemplate>
I’m converting the PlayType enum into a Brush which is why with the sample data you can see the colours that are displayed.
<converter:PlayToColour x:Key="PlayTypeToColourConverter"
ColourRun="Red" ColourPass="Blue" ColourPenalty="Yellow" ColourSpecial="Purple" />
So all of that is working ok – but obviously the representation is not as wished - looking at the example at the top, a drive consists of numerous plays, which are placed next to each other, but as a play can result in negative yardage, they are also placed underneath each other, offset by the end of the last play!
So I guess this is my question - how can I represent the data in such a form, that each play of a drive is positioned underneath the previous play of that particuler drive, but also starting at the end of that last drive (offset somehow?)
As a game consists of more than one drive I will, at a later stage, be passing on a list of “Drives” consisting of the drive starting position and a list of plays for that drive (instead of the simple list of plays in this example).
This means that the whole thing needs to be scrollable as well!
I think I’m on the right track using an ItemsControl control along with the ItemTemplate and DataTemplate, but I could sure use some expert advice as to how to achieve this.
If you've read this far, thank you for your time - GO PATS ;)
[edit]
Well thanks to AngelWPF (virtualization) and Rachel (everything!) I actually managed to get it working, at least for the list of plays I was providing the ItemsControl with. Rachel, you have a great blog. The AttachedProperties entry was very useful indeed, thanks for sharing your knowledge!
At some later stage I hope to be able to add a screenshot or two which better demonstrates what I’m waffling on about ;)
As suggested, I modified the Play class with the (amongst other properties) PlayIndex for the grid positioning
public class Play
{
public int PlayIndex { get; set; }
public int Quarter { get; set; }
public int Down { get; set; }
public int YardsToGo { get; set; }
public int Position { get; set; }
public PlayType TypeOfPlay { get; set; }
public PlayDetail DetailsOfPlay { get; set; }
public PlayResult ResultOfPlay { get; set; }
public int YardsOfPlay { get; set; }
public double PlayLength { get; set; }
public string Player1 { get; set; }
public string Player2 { get; set; }
}
and to use that PlayIndex, the ItemContainerStyle was added to the ItemsControl
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Grid.Column" Value="{Binding PlayIndex}" />
<Setter Property="Grid.Row" Value="{Binding PlayIndex}" />
</Style>
</ItemsControl.ItemContainerStyle>
Changing the ItemsPanelTemplate from a StackPanel to a Grid also helped for this :) Now each Play is displayed next to each other as well as under each other with the respective colour. Adding a ToolTip to the ItemsControl (using the other properties in the Play class) polished it off.
As I have already stated, this works great now with a list of Play items (shortened for simplicity):
List<Play> SamplePlays = new List<Play>()
{
new Play { PlayIndex = 0, Position = 30, YardsOfPlay = 5, PlayLength = 25 },
new Play { PlayIndex = 1, Position = 35, YardsOfPlay = 15, PlayLength = 75 },
new Play { PlayIndex = 2, Position = 50, YardsOfPlay = 0, PlayLength = 0 },
new Play { PlayIndex = 3, Position = 50, YardsOfPlay = 8, PlayLength = 40 },
new Play { PlayIndex = 4, Position = 58, YardsOfPlay = 1, PlayLength = 5 },
new Play { PlayIndex = 5, Position = 59, YardsOfPlay = 30, PlayLength = 150 }
};
But how would I go about binding what I had written at the end of the original post with regard to passing on a list of “Drives”?
Please note - with me having written this in the original post, I hope that this "allows" me to edit this post with further information and not ask a completely new question!
Given the following “Drive” class:
public class Drive
{
public int DriveIndex { get; set; }
public int StartPos { get; set; }
public int EndPos { get; set; }
public double DriveLength { get; set; }
public DriveResult Result { get; set; }
public List<Play> DrivePlays { get; set; }
}
Filling a list with data (using the same plays as above)
List<Drive> SampleDrives = new List<Drive>()
{
new Drive { DriveIndex = 0, StartPos = 30, EndPos = 49, DriveLength = 19,
DrivePlays = new List<Play>()
{
// create plays here
}},
new Drive { DriveIndex = 1, StartPos = 30, EndPos = 49, DriveLength = 19,
DrivePlays = new List<Play>()
{
// create plays here
}}
};
and the ViewModel now looking like this:
public class ucDriveChartVM : ViewModelBase
{
public List<Play> DriveDetails { get; set; }
public List<Drive> Drives { get; set; }
public ucDriveChartVM(ucDriveChartTP transferParameter)
{
this.DriveDetails = transferParameter.Plays;
this.Drives = transferParameter.Drives;
}
}
I obviously need to provide this ItemsControl (containing the drives) with another DataTemplate, but this DataTemplate would have to hold the plays within that list of drives. What I quickly found out is that you can’t place content in a Rectangle, so I would have to think this over :/
After experimenting a little, I bound to the DriveLength property to graphically see the length of a given drive and came up with a DataGridCell, so that I could place content in there (e.g. StackPanel).
(I know I'm not binding to anything other than the length of the drive, the Rectangle in this example is just to show something when I test!)
<DataTemplate x:Key="myDataTemplateDrives">
<StackPanel Orientation="Horizontal" Width="{Binding DriveLength}"
SnapsToDevicePixels="True" Margin="0,0,1,0">
<Border>
<Grid>
<StackPanel>
<DataGridCell>
<StackPanel Width="{Binding Path=DriveLength}">
<Rectangle Height="10" Fill="Gray" />
</StackPanel>
</DataGridCell>
</StackPanel>
</Grid>
</Border>
</StackPanel>
</DataTemplate>
But I have already gone through my fourth cup of coffee and I’M having trouble binding to the List within the List object? Something like Drives.DrivePlays?!
So I'm going from a simple (thank you both)
List<Play>
binding, to a
List<Drive>
binding, which contains a
List<Play>!
called "DrivePlays" :D
Am I missing something obvious or do I need more coffee?
edit
I've explained the last part of the problem in a seperate question
Related question regarding the last part of the original question
Let me start by saying I know nothing about football, so if I understand you incorrectly forgive me.
To start with, an ItemsControl
actually wraps each item in a ContentPresenter, so it won't work to adjust the positioning of your items from your DataTemplate
. If you wish to alter the positioning of your items, you need to use the ItemContainerStyle
(For an example, see here)
Because of the way you want to position your data, I would look into trying to add Grid.Row/Grid.Column properties to your Play
class, and figure out what those should be in the code behind. Then you can draw your ItemsControl as a Grid with Columns/Rows based on the # of Rows/Columns in your Plays
list, and place each item in the appropriate cell. (I have a sample of using Attached Properties to define Grid.Rows/Columns here, although I believe they need some minor modifications to be bound values... need to change Grid
to GridHelpers
and change Register
to RegisterAttached
)
For scrolling the ItemsControl, you can wrap it in a ScrollViewer
. If you wish to Virtualize the ItemsControl as well, check out this question
I suggest for layout alignment of data items and their virtualization you have to analyze severeal options provided by all the items controls under WPF ...
ItemsControl
withUniformGrid
asItemsPanel
.ListBox
with transparent selection brush (to make it look like a items control) provides inherent virtualization.ListView
provides both an alignment layout ofGridView
and virtualization.- WPF
DataGrid
can be used for better options such as column / row virtualization, sorting etc... - You could however, implement virtualized
ItemsControl
in version prior to 4.0, http://omniscientist.net:8081/efficientlargedatasets
精彩评论