Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,9 @@
<Compile Include="SamplePages\ImageEx\ImageExLazyLoadingControl.xaml.cs">
<DependentUpon>ImageExLazyLoadingControl.xaml</DependentUpon>
</Compile>
<Compile Include="SamplePages\ObservableGroup\ObservableGroupPage.xaml.cs">
<DependentUpon>ObservableGroupPage.xaml</DependentUpon>
</Compile>
<Compile Include="SamplePages\OnDevice\OnDevicePage.xaml.cs">
<DependentUpon>OnDevicePage.xaml</DependentUpon>
</Compile>
Expand All @@ -542,6 +545,7 @@
<Content Include="SamplePages\Eyedropper\EyedropperXaml.bind" />
<Content Include="SamplePages\Eyedropper\EyedropperCode.bind" />
<Content Include="SamplePages\TokenizingTextBox\TokenizingTextBoxCode.bind" />
<Content Include="SamplePages\ObservableGroup\ObservableGroup.bind" />
</ItemGroup>
<ItemGroup>
<Compile Include="App.xaml.cs">
Expand Down Expand Up @@ -939,6 +943,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="SamplePages\ObservableGroup\ObservableGroupPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="SamplePages\OnDevice\OnDevicePage.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Grab a sample type
public class Person
{
public string Name { get; set; }
}

// Set up the original list with a few sample items
var contacts = new[]
{
new Person { Name = "Staff" },
new Person { Name = "Swan" },
new Person { Name = "Orchid" },
new Person { Name = "Flame" },
new Person { Name = "Arrow" },
new Person { Name = "Tempest" },
new Person { Name = "Pearl" },
new Person { Name = "Hydra" },
new Person { Name = "Lamp Post" },
new Person { Name = "Looking Glass" },
};

// Group the contacts by first letter
var grouped = contacts.GroupBy(GetGroupName).OrderBy(g => g.Key);

// Create an observable grouped collection
var contactsSource = new ObservableGroupedCollection<string, Person>(grouped);

// Create a read-only observable grouped collection
// Note: This step is optional. It is usually used in view models to expose the collection as read-only and prevent the view from altering it.
var readonlyContacts = new ReadOnlyObservableGroupedCollection<string, Person>(contactsSource);

// Set up the CollectionViewSource
var cvs = new CollectionViewSource
{
IsSourceGrouped = True,
Source = readonlyContacts,
};

// Bind the CollectionViewSource to anything that supports grouped collections.
ContactsList.ItemsSource = cvs.View;
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<Page x:Class="Microsoft.Toolkit.Uwp.SampleApp.SamplePages.ObservableGroupPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:collections="using:Microsoft.Toolkit.Collections"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.Toolkit.Uwp.SampleApp.SamplePages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:system="using:System"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">

<Page.Resources>
<Style TargetType="Button">
<Setter Property="Margin" Value="8,0,0,0" />
</Style>

<DataTemplate x:Key="PersonDataTemplate"
x:DataType="local:Person">
<TextBlock Text="{x:Bind Name}" />
</DataTemplate>

<DataTemplate x:Key="GroupDataTemplate"
x:DataType="collections:IReadOnlyObservableGroup">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}"
Text="{x:Bind (x:String)Key}" />
<TextBlock Margin="8,0,0,0"
VerticalAlignment="Bottom"
Opacity="0.8"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind system:String.Format('{0} item(s)', Count), Mode=OneWay}" />
</StackPanel>
</DataTemplate>

<CollectionViewSource x:Key="cvs"
x:Name="cvs"
IsSourceGrouped="True"
Source="{x:Bind Contacts}" />
</Page.Resources>

<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<StackPanel Margin="0,4"
HorizontalAlignment="Center"
Orientation="Horizontal">
<TextBox x:Name="NewContact"
MinWidth="200"
PlaceholderText="New contact"
TextChanged="OnNewContactTextChanged"
KeyDown="OnNewContactKeyDown"/>
<Button x:Name="AddContact"
Click="OnAddContactClick"
Content="Add"
IsEnabled="False" />
<Button x:Name="RemoveContact"
Click="OnRemoveButtonClick"
Content="Remove selected"
IsEnabled="False" />
</StackPanel>

<ListView x:Name="ContactsListView"
Grid.Row="1"
ItemTemplate="{StaticResource PersonDataTemplate}"
ItemsSource="{x:Bind cvs.View}"
SelectionChanged="OnContactsListViewSelectionChanged"
SelectionMode="Single">
<ListView.GroupStyle>
<GroupStyle HeaderTemplate="{StaticResource GroupDataTemplate}" />
</ListView.GroupStyle>
</ListView>
</Grid>
</Page>
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Linq;
using Microsoft.Toolkit.Collections;
using Windows.System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages
{
/// <summary>
/// The sample page for the observable group collections.
/// </summary>
public sealed partial class ObservableGroupPage : Page
{
private readonly ObservableGroupedCollection<string, Person> _contactsSource;

public ObservableGroupPage()
{
var contacts = new[]
{
new Person { Name = "Staff" },
new Person { Name = "Swan" },
new Person { Name = "Orchid" },
new Person { Name = "Flame" },
new Person { Name = "Arrow" },
new Person { Name = "Tempest" },
new Person { Name = "Pearl" },
new Person { Name = "Hydra" },
new Person { Name = "Lamp Post" },
new Person { Name = "Looking Glass" },
};
var grouped = contacts.GroupBy(GetGroupName).OrderBy(g => g.Key);
_contactsSource = new ObservableGroupedCollection<string, Person>(grouped);
Contacts = new ReadOnlyObservableGroupedCollection<string, Person>(_contactsSource);

InitializeComponent();
}

public ReadOnlyObservableGroupedCollection<string, Person> Contacts { get; }

private static string GetGroupName(Person person) => person.Name.First().ToString().ToUpper();

private void OnContactsListViewSelectionChanged(object sender, SelectionChangedEventArgs e) => RemoveContact.IsEnabled = ContactsListView.SelectedItem is Person;

private void OnRemoveButtonClick(object sender, RoutedEventArgs e)
{
var selectedContact = (Person)ContactsListView.SelectedItem;
var selectedGroupName = GetGroupName(selectedContact);
var selectedGroup = _contactsSource.FirstOrDefault(group => group.Key == selectedGroupName);
if (selectedGroup != null)
{
selectedGroup.Remove(selectedContact);
if (!selectedGroup.Any())
{
// The group is empty. We can remove it.
_contactsSource.Remove(selectedGroup);
}
}
}

private bool CanAddContact() => !string.IsNullOrEmpty(NewContact.Text.Trim());

private void AddNewContact()
{
var newContact = new Person
{
Name = NewContact.Text.Trim(),
};

var groupName = GetGroupName(newContact);
var targetGroup = _contactsSource.FirstOrDefault(group => group.Key == groupName);
if (targetGroup is null)
{
_contactsSource.Add(new ObservableGroup<string, Person>(groupName, new[] { newContact }));
}
else
{
targetGroup.Add(newContact);
}

NewContact.Text = string.Empty;
}

private void OnAddContactClick(object sender, RoutedEventArgs e) => AddNewContact();

private void OnNewContactTextChanged(object sender, TextChangedEventArgs e) => AddContact.IsEnabled = CanAddContact();

private void OnNewContactKeyDown(object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Enter && CanAddContact())
{
AddNewContact();
}
}
}
}
10 changes: 10 additions & 0 deletions Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,16 @@
"Icon": "/SamplePages/AdvancedCollectionView/AdvancedCollectionView.png",
"DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/helpers/AdvancedCollectionView.md"
},
{
"Name": "ObservableGroup",
"Type": "ObservableGroupPage",
"Subcategory": "Data",
"About": "Allows you to easily create observable grouped collections.",
"CodeUrl": "https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit/ObservableGroup",
"CodeFile": "ObservableGroup.bind",
"Icon": "/Assets/Helpers.png",
"DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/helpers/ObservableGroup.md"
},
{
"Name": "CameraHelper",
"Type": "CameraHelperPage",
Expand Down
27 changes: 27 additions & 0 deletions Microsoft.Toolkit/Collections/IReadOnlyObservableGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.ComponentModel;

namespace Microsoft.Toolkit.Collections
{
/// <summary>
/// An interface for a grouped collection of items.
/// It allows us to use x:Bind with <see cref="ObservableGroup{TKey, TValue}"/> and <see cref="ReadOnlyObservableGroup{TKey, TValue}"/> by providing
/// a non-generic type that we can declare using x:DataType.
/// </summary>
public interface IReadOnlyObservableGroup : INotifyPropertyChanged
{
/// <summary>
/// Gets the key for the current collection, as an <see cref="object"/>.
/// It is immutable.
/// </summary>
object Key { get; }

/// <summary>
/// Gets the number of items currently in the grouped collection.
/// </summary>
int Count { get; }
}
}
57 changes: 57 additions & 0 deletions Microsoft.Toolkit/Collections/ObservableGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

namespace Microsoft.Toolkit.Collections
{
/// <summary>
/// An observable group.
/// It associates a <see cref="Key"/> to an <see cref="ObservableCollection{T}"/>.
/// </summary>
/// <typeparam name="TKey">The type of the group key.</typeparam>
/// <typeparam name="TValue">The type of the items in the collection.</typeparam>
public sealed class ObservableGroup<TKey, TValue> : ObservableCollection<TValue>, IGrouping<TKey, TValue>, IReadOnlyObservableGroup
{
/// <summary>
/// Initializes a new instance of the <see cref="ObservableGroup{TKey, TValue}"/> class.
/// </summary>
/// <param name="key">The key for the group.</param>
public ObservableGroup(TKey key)
{
Key = key;
}

/// <summary>
/// Initializes a new instance of the <see cref="ObservableGroup{TKey, TValue}"/> class.
/// </summary>
/// <param name="grouping">The grouping to fill the group.</param>
public ObservableGroup(IGrouping<TKey, TValue> grouping)
: base(grouping)
{
Key = grouping.Key;
}

/// <summary>
/// Initializes a new instance of the <see cref="ObservableGroup{TKey, TValue}"/> class.
/// </summary>
/// <param name="key">The key for the group.</param>
/// <param name="collection">The initial collection of data to add to the group.</param>
public ObservableGroup(TKey key, IEnumerable<TValue> collection)
: base(collection)
{
Key = key;
}

/// <summary>
/// Gets the key of the group.
/// </summary>
public TKey Key { get; }

/// <inheritdoc/>
object IReadOnlyObservableGroup.Key => Key;
}
}
34 changes: 34 additions & 0 deletions Microsoft.Toolkit/Collections/ObservableGroupedCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

namespace Microsoft.Toolkit.Collections
{
/// <summary>
/// An observable list of observable groups.
/// </summary>
/// <typeparam name="TKey">The type of the group key.</typeparam>
/// <typeparam name="TValue">The type of the items in the collection.</typeparam>
public sealed class ObservableGroupedCollection<TKey, TValue> : ObservableCollection<ObservableGroup<TKey, TValue>>
{
/// <summary>
/// Initializes a new instance of the <see cref="ObservableGroupedCollection{TKey, TValue}"/> class.
/// </summary>
public ObservableGroupedCollection()
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ObservableGroupedCollection{TKey, TValue}"/> class.
/// </summary>
/// <param name="collection">The initial data to add in the grouped collection.</param>
public ObservableGroupedCollection(IEnumerable<IGrouping<TKey, TValue>> collection)
: base(collection.Select(c => new ObservableGroup<TKey, TValue>(c)))
{
}
}
}
Loading