Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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 @@ -528,6 +528,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 @@ -552,6 +555,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 @@ -942,6 +946,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,39 @@
// 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
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,64 @@
<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: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"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">

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

<DataTemplate x:Key="PersonDataTemplate">
<TextBlock Text="{Binding Name}" />
</DataTemplate>

<DataTemplate x:Key="GroupDataTemplate">
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}"
Text="{Binding Key}" />
</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" />
<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,86 @@
// 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 Microsoft.Toolkit.Observables.Collections;
using System.Linq;
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 void OnAddContactClick(object sender, RoutedEventArgs e)
{
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 OnNewContactTextChanged(object sender, TextChangedEventArgs e) => AddContact.IsEnabled = !string.IsNullOrEmpty(NewContact.Text.Trim());
}
}
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
4 changes: 4 additions & 0 deletions Microsoft.Toolkit/Microsoft.Toolkit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,8 @@
</Compile>
</ItemGroup>

<ItemGroup>
<Folder Include="Observables\" />
</ItemGroup>

</Project>
54 changes: 54 additions & 0 deletions Microsoft.Toolkit/Observables/ObservableGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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.Observables.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>
{
/// <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; }
}
}
34 changes: 34 additions & 0 deletions Microsoft.Toolkit/Observables/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.Observables.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)))
{
}
}
}
41 changes: 41 additions & 0 deletions Microsoft.Toolkit/Observables/ReadOnlyObservableGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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.ObjectModel;
using System.Linq;

namespace Microsoft.Toolkit.Observables.Collections
{
/// <summary>
/// A read-only observable group. It associates a <see cref="Key"/> to a <see cref="ReadOnlyObservableCollection{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 ReadOnlyObservableGroup<TKey, TValue> : ReadOnlyObservableCollection<TValue>, IGrouping<TKey, TValue>
{
/// <summary>
/// Initializes a new instance of the <see cref="ReadOnlyObservableGroup{TKey, TValue}"/> class.
/// </summary>
/// <param name="key">The key of the group.</param>
/// <param name="collection">The collection of items to add in the group.</param>
public ReadOnlyObservableGroup(TKey key, ObservableCollection<TValue> collection)
: base(collection)
{
Key = key;
}

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

/// <inheritdoc/>
public TKey Key { get; }
}
}
Loading