Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
144 changes: 144 additions & 0 deletions ClassicAssist/UI/Misc/Behaviours/SkillsGridViewSortBehaviour.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#region License

// Copyright (C) 2025 Reetus
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY

#endregion

using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using ClassicAssist.Data.Skills;
using ClassicAssist.Shared.UI;
using Microsoft.Xaml.Behaviors;

namespace ClassicAssist.UI.Misc.Behaviours
{
public class SkillsGridViewSortBehaviour : Behavior<ListView>
{
public static readonly DependencyProperty SortChangedCommandProperty =
DependencyProperty.Register( nameof( SortChangedCommand ), typeof( ICommand ), typeof( SkillsGridViewSortBehaviour ) );

public static readonly DependencyProperty SetSortCommandProperty = DependencyProperty.Register( nameof( SetSortCommand ), typeof( ICommand ),
typeof( SkillsGridViewSortBehaviour ), new PropertyMetadata( null ) );

private ListSortDirection _lastDirection;

private SkillsGridViewColumn.Enums _lastHeaderClicked;
private ICommand _setSortCommandImpl;

public ICommand SetSortCommand
{
get => (ICommand) GetValue( SetSortCommandProperty );
set => SetValue( SetSortCommandProperty, value );
}

public ICommand SetSortCommandImpl => _setSortCommandImpl ?? ( _setSortCommandImpl = new RelayCommand( SetSort ) );

public ICommand SortChangedCommand
{
get => (ICommand) GetValue( SortChangedCommandProperty );
set => SetValue( SortChangedCommandProperty, value );
}

private void SetSort( object obj )
{
if ( !( obj is SkillsSortInfo info ) )
{
return;
}

if ( !( AssociatedObject.View is GridView gridView ) )
{
return;
}

SkillsGridViewColumn column = gridView.Columns.FirstOrDefault( e => e is SkillsGridViewColumn sgvc && sgvc.SortField == info.SortBy ) as SkillsGridViewColumn;

ApplySort( column, info.Direction );
}

protected override void OnAttached()
{
base.OnAttached();

AssociatedObject.AddHandler( ButtonBase.ClickEvent, new RoutedEventHandler( OnHeaderClick ) );

SetValue( SetSortCommandProperty, SetSortCommandImpl );
}

protected override void OnDetaching()
{
AssociatedObject.RemoveHandler( ButtonBase.ClickEvent, new RoutedEventHandler( OnHeaderClick ) );

base.OnDetaching();
}

private void OnHeaderClick( object sender, RoutedEventArgs e )
{
if ( !( e.OriginalSource is GridViewColumnHeader headerClicked ) || headerClicked.Column == null )
{
return;
}

SkillsGridViewColumn.Enums sortBy = ((SkillsGridViewColumn)headerClicked.Column).SortField;

ListSortDirection direction;

if ( sortBy == _lastHeaderClicked )
{
direction = _lastDirection == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending;
}
else
{
direction = ListSortDirection.Ascending;
}

ApplySort( headerClicked.Column as SkillsGridViewColumn, direction );

SortChangedCommand?.Execute( new SkillsSortInfo( sortBy, direction ) );
}

private void ApplySort( SkillsGridViewColumn column, ListSortDirection direction )
{
_lastDirection = direction;

_lastHeaderClicked = column.SortField;

ListCollectionView dataView = (ListCollectionView)CollectionViewSource.GetDefaultView( AssociatedObject.ItemsSource );

if ( dataView == null )
{
return;
}

dataView.SortDescriptions.Clear();
dataView.SortDescriptions.Add( new SortDescription( column.SortField.ToString(), direction ) );
dataView.CustomSort = new SkillComparer( direction, column.SortField );
Comment on lines +126 to +128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Stop deriving the sort key from the header text

column.Header.ToString() yields captions such as "+/-" (see ClassicAssist/UI/Views/SkillsTabControl.xaml Line 74) and other localised strings, but ListCollectionView.SortDescriptions.Add requires an actual property identifier. Clicking the "+/-" column now throws an ArgumentException because there is no property named "+/-" on SkillEntry. Please drive the sort description from a real property name (e.g. map the enum to the underlying property) or drop the SortDescriptions.Add call and rely solely on CustomSort, so users can sort the delta/status columns without crashing the tab.

-            dataView.SortDescriptions.Add( new SortDescription( column.Header.ToString(), direction ) );
🤖 Prompt for AI Agents
In ClassicAssist/UI/Misc/Behaviours/SkillsGridViewSortBehaviour.cs around lines
126-128, the code builds a SortDescription using column.Header.ToString() which
can be UI text like "+/-" and causes ArgumentException; replace that by using a
real property identifier: if column.SortField (or an equivalent property name)
is non-empty and corresponds to a SkillEntry property, add the SortDescription
with that name, otherwise skip adding SortDescriptions entirely and rely solely
on dataView.CustomSort = new SkillComparer(direction, column.SortField); ensure
you clear SortDescriptions first as before and only call SortDescriptions.Add
when the SortField is valid.

dataView.Refresh();
}
}

public class SkillsSortInfo
{
public SkillsSortInfo( SkillsGridViewColumn.Enums sortBy, ListSortDirection direction )
{
SortBy = sortBy;
Direction = direction;
}

public ListSortDirection Direction { get; }
public SkillsGridViewColumn.Enums SortBy { get; }
}
}
44 changes: 43 additions & 1 deletion ClassicAssist/UI/ViewModels/SkillsTabViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows.Input;
Expand All @@ -13,6 +14,7 @@
using ClassicAssist.Shared.Resources;
using ClassicAssist.Shared.UI;
using ClassicAssist.UI.Misc;
using ClassicAssist.UI.Misc.Behaviours;
using ClassicAssist.UO;
using ClassicAssist.UO.Data;
using ClassicAssist.UO.Network;
Expand All @@ -29,6 +31,9 @@ public class SkillsTabViewModel : BaseViewModel, IGlobalSettingProvider
private SkillEntry _selectedItem;
private ICommand _setAllSkillLocksCommand;
private ICommand _setSkillLocksCommand;
private ICommand _sortChangedCommand;

private SkillsSortInfo _sortInfo;
private float _totalBase;
private ICommand _useSkillCommand;

Expand Down Expand Up @@ -79,6 +84,10 @@ public float TotalBase
_useSkillCommand ?? ( _useSkillCommand =
new RelayCommand( UseSkill, o => SelectedItem?.Skill.Invokable ?? false ) );

public ICommand SetSortCommand { get; set; }

public ICommand SortChangedCommand => _sortChangedCommand ?? ( _sortChangedCommand = new RelayCommand( OnSortChanged ) );

public void Serialize( JObject json, bool global = false )
{
JArray skills = new JArray();
Expand All @@ -105,6 +114,15 @@ public void Serialize( JObject json, bool global = false )
}

json.Add( "Skills", skills );

if ( _sortInfo == null || global )
{
return;
}

JObject obj = new JObject { { "SortField", _sortInfo.SortBy.ToString() }, { "SortDirection", _sortInfo.Direction.ToString() } };

json.Add( "SkillsSort", obj );
}

public void Deserialize( JObject json, Options options, bool global = false )
Expand Down Expand Up @@ -191,13 +209,37 @@ public void Deserialize( JObject json, Options options, bool global = false )
}

hotkey.AddCategory( _hotkeyCategory );

if ( !( json["SkillsSort"] is JObject obj ) || obj["SortField"] == null || obj["SortDirection"] == null || global )
{
return;
}

if ( !Enum.TryParse( obj["SortField"].ToObject<string>(), out SkillsGridViewColumn.Enums sortBy ) ||
!Enum.TryParse( obj["SortDirection"].ToObject<string>(), out ListSortDirection direction ) )
{
return;
}

_sortInfo = new SkillsSortInfo( sortBy, direction );
SetSortCommand?.Execute( _sortInfo );
}

public string GetGlobalFilename()
{
return "Skills.json";
}

private void OnSortChanged( object obj )
{
if (!( obj is SkillsSortInfo info ) )
{
return;
}

_sortInfo = info;
}

private void ResetDeltas( object obj )
{
foreach ( SkillEntry skillEntry in Items )
Expand Down
7 changes: 6 additions & 1 deletion ClassicAssist/UI/Views/SkillsTabControl.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
xmlns:valueConverters="clr-namespace:ClassicAssist.UI.Misc.ValueConverters"
xmlns:sharedResources="clr-namespace:ClassicAssist.Shared.Resources;assembly=ClassicAssist.Shared"
xmlns:skills="clr-namespace:ClassicAssist.Data.Skills"
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
xmlns:behaviours="clr-namespace:ClassicAssist.UI.Misc.Behaviours"
mc:Ignorable="d">
<d:DesignerProperties.DesignStyle>
<Style TargetType="UserControl">
Expand Down Expand Up @@ -57,7 +59,6 @@
</ContextMenu>
</Grid.Resources>
<ListView Grid.Column="0" ItemsSource="{Binding Items}" Margin="5"
GridViewColumnHeader.Click="GridViewHeaderOnClick" x:Name="listView"
SelectedItem="{Binding SelectedItem}" ContextMenu="{StaticResource ItemContextMenu}">
<ListView.View>
<GridView>
Expand Down Expand Up @@ -88,6 +89,10 @@
</misc1:SkillsGridViewColumn>
</GridView>
</ListView.View>
<b:Interaction.Behaviors>
<behaviours:SkillsGridViewSortBehaviour SortChangedCommand="{Binding SortChangedCommand}"
SetSortCommand="{Binding SetSortCommand, Mode=OneWayToSource}" />
</b:Interaction.Behaviors>
</ListView>
<StackPanel Grid.Column="1" Margin="10,0,10,10">
<StackPanel.Resources>
Expand Down
53 changes: 1 addition & 52 deletions ClassicAssist/UI/Views/SkillsTabControl.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using ClassicAssist.Data.Skills;
using ClassicAssist.UI.Misc;
using System.Windows.Controls;

namespace ClassicAssist.UI.Views
{
Expand All @@ -12,55 +7,9 @@ namespace ClassicAssist.UI.Views
/// </summary>
public partial class SkillsTabControl : UserControl
{
private ListSortDirection _lastDirection;
private GridViewColumnHeader _lastHeaderClicked;

public SkillsTabControl()
{
InitializeComponent();
}

private void GridViewHeaderOnClick( object sender, RoutedEventArgs e )
{
GridViewColumnHeader headerClicked = e.OriginalSource as GridViewColumnHeader;
ListSortDirection direction;

if ( headerClicked == null || headerClicked.Role == GridViewColumnHeaderRole.Padding )
{
return;
}

if ( !Equals( headerClicked, _lastHeaderClicked ) )
{
direction = ListSortDirection.Ascending;
}
else
{
direction = _lastDirection == ListSortDirection.Ascending
? ListSortDirection.Descending
: ListSortDirection.Ascending;
}

if ( headerClicked.Column is SkillsGridViewColumn column )
{
Sort( (string) headerClicked.Column.Header, direction, column.SortField );
}

_lastHeaderClicked = headerClicked;
_lastDirection = direction;
}

private void Sort( string header, ListSortDirection direction, SkillsGridViewColumn.Enums sortField )
{
ListCollectionView dataView =
(ListCollectionView) CollectionViewSource.GetDefaultView( listView.ItemsSource );

dataView.SortDescriptions.Clear();
SortDescription sd = new SortDescription( header, direction );
dataView.SortDescriptions.Add( sd );

dataView.CustomSort = new SkillComparer( direction, sortField );
dataView.Refresh();
}
}
}
Loading