I was wondering if anybody knew how to display only a few items from a bound collection within an ItemsControl. Whether it be by filtering an ICollectionView or other means. I am sure I could come up with a long-winded solution on my own but I would like to see what's out there already.
Basically, I have an ItemsControl bound to a collection of objects contained in a Model. What I would like to do is display only a few of those items, then have a hyperlink/button for a "view more". Which will display the entire collection of items. I was hoping to be able to use the VSM to signal 'collapsed' and 'expanded' states but I'm having problems wrapping my head around how to initialize the list. Because the binding is created in XAML, I'm trying to avoid using Linq in the codebehind to manually modify the ItemsSource collection, which could be a solution if all else fails.
I could display some code if necessary but I don't think it would be any more helpful than my explanation. Again, I'm just hoping somebody out there has done something similar before I experiment too much and end up breaking my viewmodel lol.
Thanks in advance.
[UPDATE] - This is the solution I've come up with after much brainstorming (for anyone else wishing to do the same thing). Thanks to AnthonyWJones for the idea.
What I've done is put together a generic 'model' which acts as a bridge between a model's source collection and the 'view' collection. The intended purpose (for me) was to extend any model class generated by WCF RIA Service that may have comments associated with it while using the same UI (controls and templates), so the expected collection is an EntityCollection where T is an instance of 'Entity'
All of the following classes are declared in the Silverlight client project
First a little plumbing:
// this is so we can reference our model without generic arguments
public interface ICommentModel : INotifyPropertyChanged
{
Int32 TotalComments { get; }
Int32 VisibleComments { get; }
Boolean IsExpanded { get; set; }
Boolean IsExpandable { get; }
ICommand ExpandCommand { get; }
IEnumerable Collection { get; }
}
// the command we'll use to expand our collection
public class ExpandCommand : ICommand
{
ICommentModel model;
public ExpandCommand(ICommentModel model) {
this.model = model;
this.model.PropertyChanged += ModelPropertyChanged;
}
public bool CanExecute(object parameter) {
return this.model.IsExpandable;
}
public void Execute(object parameter) {
this.model.IsExpanded = !this.model.IsExpanded;
}
private void ModelPropertyChanged(object sender, PropertyChangedEventArgs e) {
if (e.PropertyName == "IsExpandable")
RaiseCanExecuteChanged();
}
private void RaiseCanExecuteChanged() {
var execute = CanExecuteChanged;
if (execute != null) execute(this, EventArgs.Empty);
}
public event EventHandler CanExecuteChanged;
}
// and finally.. the big guns
public class CommentModel<TEntity> : ICommentModel
where TEntity : Entity
{
Boolean isExpanded;
ICommand expandCommand;
IEnumerable<TEntity> source;
IEnumerable<TEntity> originalSource;
public Int32 TotalComments { get { return originalSource.Count(); } }
public Int32 VisibleComments { get { return source.Count(); } }
public Boolean IsExpanded {
get { return isExpanded; }
set { isExpanded = value; OnIsExpandedChanged(); }
}
public Boolean IsExpandable {
get { return (!IsExpanded && originalSource.Count() > 2); }
}
public ICommand ExpandCommand {
get { return expandCommand; }
}
public IEnumerable Collection { get { return source; } }
public CommentModel(EntityCollection<TEntity> source) {
expandCommand = new ExpandCommand(this);
source.EntityAdded += OriginalSourceChanged;
source.EntityRemoved += OriginalSourceChanged;
originalSource = source;
UpdateBoundCollection();
}
private void OnIsExpandedChanged() {
OnPropertyChanged("IsExpanded");
UpdateBoundCollection();
}
private void OriginalSourceChanged(object sender, EntityCollectionChangedEventArgs<TEntity> e) {
OnPropertyChanged("TotalComments");
UpdateBoundCollection();
}
private void UpdateBoundCollection() {
if (IsExpanded)
source = originalSource.OrderBy(s => PropertySorter(s));
else
source = originalSource.OrderByDescending(s => PropertySorter(s)).Take(2).OrderBy(s => PropertySorter(s));
OnPropertyChanged("IsExpandable");
OnPropertyChanged("VisibleComments");
OnPropertyChanged("Collection");
}
// I wasn't sure how to get instances Func<T,TRet> into this class
// without some dirty hacking, so I used some reflection to run "OrderBy" queries
// All entities in my DataModel have 'bigint' Id columns
private long PropertySorter(TEntity s) {
var props = from x in s.GetType().GetProperties()
where x.Name == "Id"
select x;
if (props.Count() > 0)
return (long)props.First().GetValue(s, null);
return 0;
}
protected virtual void OnPropertyChanged(string propName) {
var x = PropertyChanged;
if (x != null) x(this, new PropertyChangedEventArgs(propName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
And now we need to use it. WCF RIA Services generates classes marked as partial (I don't know if there are situations when it doesn't, but from what I've seen it does). So we'll extend the entity class it generates to include our new model.
// this must be inside the same namespace the classes are generated in
// generally this is <ProjectName>.Web
public partial class Timeline
{
ICommentModel model;
public ICommentModel CommentModel {
get {
if (model == null)
model = new CommentModel<TimelineComment>(Comments);
return model;
}
}
}
Now we can reference the comment model in bindings where the 'Timeline' class is the data/binding context.
Example:
<UserControl x:Class="Testing.Comments"
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"
mc:Ignorable="d"
d:DesignHeight="291" d:DesignWidth="382">
<Border CornerRadius="2" BorderBrush="{StaticResource LineBrush}" BorderThickness="1">
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Visibility="{Binding Path=CommentModel.IsExpandable, Converter={StaticResource BooleanToVisibility}}">
<HyperlinkButton
FontSize="10"
Command="{Binding Path=CommentModel.ExpandCommand}"
Background="{StaticResource BackBrush}">
<TextBlock>
<Run Text="View all"/>
<Run Text="{Binding Path=CommentModel.TotalComments}"/>
<Run Text="comments"/>
</TextBlock>
</HyperlinkButton>
<Rectangle Height="1" Margin="0,1,0,0" Fill="{StaticResource LineBrush}" VerticalAlignment="Bottom"/>
</StackPanel>
<ItemsControl
Grid.Row="1"
ItemsSource="{Binding Path=CommentModel.Collection}"
ItemTemplate="{StaticResource CommentTemplate}" />
</Grid>
</Border>
</UserControl>
This is a job for your ViewModel. Internally you have a full collection of items. However initially the ViewModel should expose an IEnumerable
that will make only a few available.
The ViewModel will also expose an ICommand
property called "ListAll". When executed this command will replace the exposed IEnumerable
with one that will list all the items.
Now its a simple case of binding the ItemsControl as you are already doing and adding a "More" button bound the "ListAll" command.