Skip to content

Commit

Permalink
Merge pull request #139 from Insire/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
Insire authored Apr 10, 2021
2 parents ef5d54b + 543b392 commit 6973b4f
Show file tree
Hide file tree
Showing 14 changed files with 364 additions and 203 deletions.
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,23 @@ Pre release nuget packages are available [here](https://pkgs.dev.azure.com/SoftT
- Support for loading a System.Drawing.Bitmap into a BitmapSource without copying its data
- Attached Properties
- Filter
- bind the predicate of a CollectionView
- refresh a CollectionView on property change
- binding ICollectionView.Filter
- refresh ICollectionView on property change
- Focus
- set focus on window load
- TriggerActions
- ClearPasswordBoxAction
- ClearTextBoxAction
- Behaviors
- MultiSelectionBehavior for ListBoxes
- AutoscrollBehavior for ListBoxes
- SelectedTreeViewItemBehavior
- PasswordBindingBehavior
- MultiSelectionBehavior: bind MultiSelector.SelectedItems (works for DataGrid and ListBox)
- SelectedTreeViewItemBehavior: bind SelectedItem for TreeView
- PasswordBindingBehavior: bind PasswordBox.Password
- WatermarkBehavior for TextBoxes
- AutoRepositionPopupBehavior
- LaunchNavigateUriAsNewProcessBehavior: Launch Hyperlink.NavigateUri in a new Process
- ScrollSelectionIntoViewBehavior: scroll SelectedItem into View for DataGrid and ListBox
- ScrollToEndCommandBehavior: execute a command, when a scrollviewer has been scrolled to its vertical or horizontal end
- AutoScrollBehavior: auto scrolling for Selector (e.g. ListBox and DataGrid)
- Controls
- FileSystemBrowser
- VirtualizingTilePanel
Expand Down
13 changes: 8 additions & 5 deletions src/MvvmScarletToolkit.Wpf.Samples/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,8 @@
SelectedItem="{Binding SelectedItem}">

<i:Interaction.Behaviors>
<mvvm:ScrollToEndCommandBehavior Command="{Binding NextCommand}" />
<mvvm:ScrollToEndCommandBehavior Command="{Binding NextCommand}" Interval="00:00:00.250" />
<mvvm:AutoScrollBehavior />
</i:Interaction.Behaviors>

<DataGrid.GroupStyle>
Expand Down Expand Up @@ -575,6 +576,7 @@
<TextBlock Margin="8,0" HorizontalAlignment="Center">
<Run Text="{Binding CurrentPage, StringFormat='{}Page {0}', Mode=OneWay}" />
<Run Text="{Binding TotalPageCount, StringFormat='{}of {0}', Mode=OneWay}" />
<Run Text="{Binding Items.Count, StringFormat='{}(Entries {0})', Mode=OneWay}" />
</TextBlock>
</DockPanel>

Expand Down Expand Up @@ -661,8 +663,10 @@
<DockPanel>
<DockPanel.Resources>
<CollectionViewSource x:Key="SortedViewSource"
mvvm:Filter.By="{Binding Filter}"
mvvm:Filter.RefreshWhenChanged="{Binding FilterText}"
IsLiveSortingRequested="True"
Source="{Binding SelectedItems}">
Source="{Binding Items}">
<CollectionViewSource.SortDescriptions>
<componentmodel:SortDescription Direction="Descending" PropertyName="IsSelected" />
<componentmodel:SortDescription Direction="Ascending" PropertyName="DisplayName" />
Expand All @@ -676,6 +680,7 @@
<i:InvokeCommandAction Command="{Binding LoadCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>

<Button Margin="5"
Command="{Binding LoadCommand}"
Content="Load"
Expand Down Expand Up @@ -779,10 +784,8 @@
<ListBox Grid.Row="2"
Grid.RowSpan="3"
Grid.Column="0"
mvvm:Filter.By="{Binding Filter}"
mvvm:Filter.RefreshWhenChanged="{Binding FilterText}"
ItemTemplate="{StaticResource DemoItemTemplate}"
ItemsSource="{Binding Items}"
ItemsSource="{Binding Source={StaticResource SortedViewSource}}"
SelectedItem="{Binding SelectedItem}"
SelectionMode="Extended">
<i:Interaction.Behaviors>
Expand Down
156 changes: 84 additions & 72 deletions src/MvvmScarletToolkit.Wpf/Attached Properties/Filter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,55 +6,71 @@

namespace MvvmScarletToolkit
{
// source: https://stackoverflow.com/questions/15358113/wpf-filter-a-listbox/39438710#39438710
/// <summary>
/// Attached properties that enable binding <see cref="ICollectionView.Filter"/> and calling <see cref="ICollectionView.Refresh"/> when a property changes with XAML only
/// </summary>
/// <remarks>
/// required namespaces:
/// <list type="bullet">
/// <item>
/// <description>xmlns:mvvm="http://SoftThorn.MvvmScarletToolkit.com/winfx/xaml/shared"</description>
/// </item>
/// </list>
/// </remarks>
// original idea from: https://stackoverflow.com/questions/15358113/wpf-filter-a-listbox/39438710#39438710
public static class Filter
{
/// <summary>
/// the bindable predicate thats executed for filtering a <see cref="ICollectionView"/>
/// </summary>
/// <remarks>
/// can be set on:
/// <list type="bullet">
/// <item>
/// <description><see cref="CollectionViewSource"/> and sub types</description>
/// </item>
/// <item>
/// <description><see cref="ItemsControl"/> and sub types</description>
/// </item>
/// </list>
/// </remarks>
public static readonly DependencyProperty ByProperty = DependencyProperty.RegisterAttached(
"By",
typeof(Predicate<object>),
typeof(Filter),
new PropertyMetadata(default(Predicate<object>), OnByChanged));

/// <summary>Helper for setting <see cref="ByProperty"/> on <paramref name="element"/>.</summary>
/// <param name="element"><see cref="ItemsControl"/> to set <see cref="ByProperty"/> on.</param>
/// <param name="element"><see cref="DependencyObject"/> to set <see cref="ByProperty"/> on.</param>
/// <param name="value">By property value.</param>
public static void SetBy(ItemsControl element, Predicate<object> value)
public static void SetBy(DependencyObject element, Predicate<object> value)
{
element.SetValue(ByProperty, value);
}

/// <summary>Helper for getting <see cref="ByProperty"/> from <paramref name="element"/>.</summary>
/// <param name="element"><see cref="ItemsControl"/> to read <see cref="ByProperty"/> from.</param>
/// <param name="element"><see cref="DependencyObject"/> to read <see cref="ByProperty"/> from.</param>
/// <returns>By property value.</returns>
[AttachedPropertyBrowsableForType(typeof(ItemsControl))]
public static Predicate<object> GetBy(ItemsControl element)
[AttachedPropertyBrowsableForType(typeof(DependencyObject))]
public static Predicate<object> GetBy(DependencyObject element)
{
return (Predicate<object>)element.GetValue(ByProperty);
}

public static readonly DependencyProperty ViewByProperty = DependencyProperty.RegisterAttached(
"ViewBy",
typeof(Predicate<object>),
typeof(Filter),
new PropertyMetadata(default(Predicate<object>), OnViewByChanged));

/// <summary>Helper for setting <see cref="ViewByProperty"/> on <paramref name="element"/>.</summary>
/// <param name="element"><see cref="CollectionViewSource"/> to set <see cref="ViewByProperty"/> on.</param>
/// <param name="value">ViewBy property value.</param>
public static void SetViewBy(CollectionViewSource element, Predicate<object> value)
{
element.SetValue(ViewByProperty, value);
}

/// <summary>Helper for getting <see cref="ViewByProperty"/> from <paramref name="element"/>.</summary>
/// <param name="element"><see cref="CollectionViewSource"/> to read <see cref="ViewByProperty"/> from.</param>
/// <returns>ViewBy property value.</returns>
[AttachedPropertyBrowsableForType(typeof(CollectionViewSource))]
public static Predicate<object> GetViewBy(CollectionViewSource element)
{
return (Predicate<object>)element.GetValue(ViewByProperty);
}

/// <summary>
/// the property that we listen for changes on, so that we can refresh its <see cref="ICollectionView"/>
/// </summary>
/// <remarks>
/// can be set on:
/// <list type="bullet">
/// <item>
/// <description><see cref="CollectionViewSource"/> and sub types</description>
/// </item>
/// <item>
/// <description><see cref="ItemsControl"/> and sub types</description>
/// </item>
/// </list>
/// </remarks>
public static readonly DependencyProperty RefreshWhenChangedProperty = DependencyProperty.RegisterAttached(
"RefreshWhenChanged",
typeof(object),
Expand All @@ -64,16 +80,16 @@ public static Predicate<object> GetViewBy(CollectionViewSource element)
/// <summary>Helper for setting <see cref="RefreshWhenChangedProperty"/> on <paramref name="element"/>.</summary>
/// <param name="element"><see cref="ItemsControl"/> to set <see cref="RefreshWhenChangedProperty"/> on.</param>
/// <param name="value">RefreshWhenChanged property value.</param>
public static void SetRefreshWhenChanged(ItemsControl element, object value)
public static void SetRefreshWhenChanged(DependencyObject element, object value)
{
element.SetValue(RefreshWhenChangedProperty, value);
}

/// <summary>Helper for getting <see cref="RefreshWhenChangedProperty"/> from <paramref name="element"/>.</summary>
/// <param name="element"><see cref="ItemsControl"/> to read <see cref="RefreshWhenChangedProperty"/> from.</param>
/// <returns>RefreshWhenChanged property value.</returns>
[AttachedPropertyBrowsableForType(typeof(ItemsControl))]
public static object GetRefreshWhenChanged(ItemsControl element)
[AttachedPropertyBrowsableForType(typeof(DependencyObject))]
public static object GetRefreshWhenChanged(DependencyObject element)
{
return element.GetValue(RefreshWhenChangedProperty);
}
Expand All @@ -87,70 +103,66 @@ private static void OnByChanged(DependencyObject d, DependencyPropertyChangedEve

if (d is ItemsControl itemsControl && itemsControl.Items.CanFilter)
{
FilterItemsControl(itemsControl, predicate);
return;
}
}

private static void OnViewByChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(e.NewValue is Predicate<object> predicate))
{
AttachFilterToCollectionView(itemsControl.Items, predicate);
return;
}

switch (d)
if (d is CollectionViewSource collectionViewSource)
{
case CollectionViewSource source:
if (collectionViewSource.View is null)
{
var propertyDescriptor = DependencyPropertyDescriptor.FromProperty(CollectionViewSource.ViewProperty, typeof(CollectionViewSource));
propertyDescriptor?.RemoveValueChanged(collectionViewSource, OnViewChanged);
propertyDescriptor?.AddValueChanged(collectionViewSource, OnViewChanged);

using (source.DeferRefresh())
void OnViewChanged(object sender, EventArgs args)
{
source.Filter -= FilterAdapter;
source.Filter += FilterAdapter;
}
break;

case ICollectionView view:
FilterCollectioNView(view, predicate);
break;
}
if (collectionViewSource.View is null)
{
return;
}

void FilterAdapter(object sender, FilterEventArgs e)
{
if (!(sender is CollectionViewSource source))
AttachFilterToCollectionView(collectionViewSource.View, predicate);
}
}
else
{
return;
AttachFilterToCollectionView(collectionViewSource.View, predicate);
}

e.Accepted = predicate(e.Item);
}
}

private static void FilterItemsControl(ItemsControl itemsControl, Predicate<object> predicate)
{
FilterCollectioNView(itemsControl.Items, predicate);
}

private static void FilterCollectioNView(ICollectionView view, Predicate<object> predicate)
private static void AttachFilterToCollectionView(ICollectionView view, Predicate<object> predicate)
{
using (view.DeferRefresh())
{
view.Filter = predicate;
if (view.Filter is null)
{
view.Filter = predicate;
}
else
{
view.Filter -= predicate;
view.Filter += predicate;
}
}
}

private static void OnRefreshWhenChangedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is ItemsControl itemsControl) || !itemsControl.Items.CanFilter)
if (d is CollectionViewSource collectionViewSource && collectionViewSource.View?.CanFilter == true)
{
return;
collectionViewSource.View.Refresh();
}

// we dont care what property was bound to this,
// we only care that it changed. When it changes we refresh the view to force updating the filter
var view = CollectionViewSource.GetDefaultView(itemsControl.ItemsSource) as CollectionView;
if (d is ItemsControl itemsControl && itemsControl.Items?.CanFilter == true)
{
// we dont care what property was bound to this,
// we only care that it changed. When it changes we refresh the view to force updating the filter
var view = CollectionViewSource.GetDefaultView(itemsControl.ItemsSource) as CollectionView;

view?.Refresh();
view?.Refresh();
}
}
}
}
41 changes: 20 additions & 21 deletions src/MvvmScarletToolkit.Wpf/Behaviors/AutoRepositionPopupBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,34 @@
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Media3D;

namespace MvvmScarletToolkit
{
/// <summary>
/// Behavior that enables synchronizing a <see cref="Popup"/>s position, when the <see cref="Window"/> its attached to, is being moved
/// </summary>
/// <remarks>
/// required namespaces:
/// <list type="bullet">
/// <item>
/// <description>xmlns:i="http://schemas.microsoft.com/xaml/behaviors"</description>
/// </item>
/// <item>
/// <description>xmlns:mvvm="http://SoftThorn.MvvmScarletToolkit.com/winfx/xaml/shared"</description>
/// </item>
/// </list>
/// </remarks>
// source: https://putridparrot.com/blog/automatically-update-a-wpf-popup-position/
public class AutoRepositionPopupBehavior : Behavior<Popup>
// usage:
// <i:Interaction.Behaviors>
// <mvvm:AutoRepositionPopupBehavior />
// </ i:Interaction.Behaviors>
public sealed class AutoRepositionPopupBehavior : Behavior<Popup>
{
private const int WM_MOVING = 0x0216;

private IDisposable? _disposableHandle;

// should be moved to a helper class
private static DependencyObject GetTopmostParent(DependencyObject element)
{
var current = element;
var result = element;

while (current != null)
{
result = current;
current = (current is Visual || current is Visual3D) ?
VisualTreeHelper.GetParent(current)
: LogicalTreeHelper.GetParent(current);
}

return result;
}

protected override void OnAttached()
{
base.OnAttached();
Expand All @@ -57,7 +56,7 @@ private void AssociatedObject_LayoutUpdated(object? sender, EventArgs? e)

private void AssociatedObject_Loaded(object? sender, RoutedEventArgs? e)
{
var root = GetTopmostParent(AssociatedObject.PlacementTarget);
var root = AssociatedObject.PlacementTarget.FindParent<Window>();
if (!(root is Window window))
{
return;
Expand Down
Loading

0 comments on commit 6973b4f

Please sign in to comment.