Объединение нескольких условных предложений LINQ Orderby(Descending) и thenBy(Descending)

Я столкнулся с проблемой при попытке реализовать логику сортировки:

Представьте себе модель Person, состоящую из нескольких одинаковых полей:

public class Person
{
    public string firstName {  get; set; }
    public string lastName { get; set; }
    public DateTime DOT {  get; set; }
    // ...
}

и еще пара человек:

ObservableCollection<Person> Persons = new();

var jane = new Person()
{
    firstName = "Jane",
    lastName = "Doe",
    DOT = DateTime.Parse("01/01/2002")
};

var john = new Person()
{
    firstName = "John",
    lastName = "Doe",
    DOT = DateTime.Parse("31/12/2002")
};

var george = new Person()
{
    firstName = "George",
    lastName = "Orwell",
    DOT = DateTime.Parse("25/06/1903")
};

// Add persons to a collection:
Persons.add(jane);
Persons.add(john);
Persons.add(george);

Теперь несколько человек хранятся в коллекции и должны быть отсортированы. Однако, пользователь должен иметь возможность выбирать, для какого поля должна применяться сортировка по возрастанию или убыванию, в каком порядке и если вообще должна применяться.

Несколько примеров сортировки людей:

  • Сценарий 1: Имя (по возрастанию)
  • Сценарий 2: Имя (по возрастанию) + фамилия (по убыванию).
  • Сценарий 3: Имя (по убыванию) + ТОЧКА (по возрастанию)
  • Сценарий 4: Имя (по убыванию) + Фамилия (по возрастанию) + ТОЧКА (по возрастанию)
  • Сценарий 5: ТОЧКА (по возрастанию) + Имя (по убыванию)
  • Сценарий 6: Фамилия (по убыванию) + Имя (по убыванию) + ТОЧКА (по возрастанию)
  • ...

Порядок упорядочивает приоритет, поэтому первая выбранная сортировка имеет более высокий приоритет, чем последующие сортировки.

Теперь, насколько я понимаю, с помощью LINQ можно сделать что-то вроде следующего:

  • Сценарий 1:
Persons = new(
    Persons.OrderBy(x => x.firstName)
           .ToList()
);
  • Сценарий 2:
Persons = new(
    Persons.OrderBy(x => x.firstName)
           .ThenByDescending(x => x.lastName)
           .ToList()
);
  • Сценарий 3:
Persons = new(
    Persons.OrderByDescending(x => x.firstName)
           .ThenBy(x => x.DOT)
           .ToList()
);
  • Сценарий 4:
Persons = new(
    Persons.OrderByDescending(x => x.firstName)
           .ThenBy(x => x.lastName)
           .ThenBy(x => x.DOT)
           .ToList()
);
  • Сценарий 5:
Persons = new(
    Persons.OrderBy(x => x.DOT)
           .ThenByDescending(x => x.firstName)
           .ToList()
);
  • Сценарий 6:
Persons = new(
    Persons.OrderByDescending(x => x.lastName)
           .ThenByDescending(x => x.firstName)
           .ThenBy(x => x.DOT)
           .ToList()
);

Мой вопрос сейчас: Как объединить эти заказы в одну чистую функцию? Как я могу динамически выбирать любой порядок по возрастанию. или дес. например, просматривая список сортировок?

ОБНОВЛЯТЬ: Для наглядности попробую привести реальный пример: Представьте, что сортировка управляется кнопками. При нажатии кнопки изменяется соответствующее логическое состояние, которое представляет соответствующее состояние по возрастанию/убыванию поля Person, например.

public class ButtonObject
{
    public string? ButtonText { get; set; }
    public IRelayCommand? ButtonCommand { get; set; }
    public bool? ButtonIsVisible { get; set; } = true;
    public bool? IsOrderedAscending { get; set; } = true;
    public string? ButtonGlyph { get; set; }
}

Таким образом, в соответствии с именем, фамилией и DOT будут созданы следующие три кнопки, которые также используют определенные поля поддержки:


// Up- and Down-Button Glyphs from Google Material Icons:
private const string Keyboard_arrow_down = "\ue313";
public const string Keyboard_arrow_up = "\ue316";

private const string 

[ObservableProperty]
private ObservableCollection<ButtonObject> _buttons = new();

// in the Constructor of the ViewModel:
{

_firstNameButton = new ButtonObject
{
    ButtonText = "FirstName",
    ButtonCommand = OrderByFirstNameCommand,
    ButtonGlyph = Keyboard_arrow_down,
};

_lastNameButton = new ButtonObject
{
    ButtonText = "LastName",
    ButtonCommand = OrderByLastNameCommand,
    ButtonGlyph = Keyboard_arrow_down,
};

_DOTButton = new ButtonObject
{
    ButtonText = "DOT",
    ButtonCommand = OrderByDOTCommand,
    ButtonGlyph = Keyboard_arrow_down,
};

// add all buttons to the collection:
Buttons.Add(_firstNameButton);
Buttons.Add(_lastNameButton);
Buttons.Add(_DOTButton);
}

Теперь представьте, что кнопки можно динамически добавлять в соответствующий CollectionView в представлении, поэтому порядок может быть таким:

<------------------>  <------------------>  <------------------> 
| FirstName Button |  | LastName Button  |  | DOT Button       |
<------------------>  <------------------>  <------------------>

или это может быть:

<------------------>  <------------------>  <------------------> 
| LastNameButton   |  | FirstNameButton  |  | DOT Button       |
<------------------>  <------------------>  <------------------>

или даже:

<------------------>  <------------------> 
| DOT Button       |  | LastName Button  |
<------------------>  <------------------>

При нажатии на кнопки изменяется логическое свойство по возрастанию, а также соответствующий глиф, чтобы соответствовать порядку возрастания или убывания:

[RelayCommand]
private void OnSortByFirstName()
{
    var _sortByFirstNameButton = Buttons[0];
    if (_sortByFirstNameButton.IsSortedAscending)
    {
        _sortByFirstNameButton.IsSortedAscending = false;
        _sortByFirstNameButton.ButtonGlyph = Keyboard_arrow_up;
    }
    else
    {
        _sortByFirstNameButton.IsSortedAscending = true;
        _sortByFirstNameButton.ButtonGlyph = Keyboard_arrow_down;
    }

    UpdateSorting();
}

[RelayCommand]
private void OnSortByLastName()
{
    var _sortByLastNameButton = Buttons[1];
    if (_sortByLastNameButton.IsSortedAscending)
    {
        _sortByLastNameButton.IsSortedAscending = false;
        _sortByLastNameButton.ButtonGlyph = Keyboard_arrow_up;
    }
    else
    {
        _sortByLastNameButton.IsSortedAscending = true;
        _sortByLastNameButton.ButtonGlyph = Keyboard_arrow_down;
    }
    UpdateSorting();
}

[RelayCommand]
private void OnSortByDOT()
{
    var _sortByDOTButton = Buttons[2];
    if (_sortByDOTButton.IsSortedAscending)
    {
        _sortByDOTButton.IsSortedAscending = false;
        _sortByDOTButton.ButtonGlyph = Keyboard_arrow_up;
    }
    else
    {
        _sortByDOTButton.IsSortedAscending = true;
        _sortByDOTButton.ButtonGlyph = Keyboard_arrow_down;
    }
    UpdateSorting();
}

Теперь меня беспокоит функция UpdateSorting(). Ему необходимо учитывать порядок и состояние всех кнопок и, следовательно, переставлять людей.

Если бы была только одна кнопка, это могло бы быть что-то вроде этого:

private void UpdateSorting()
{
    if (Buttons[0].IsSortedAscending)
    {
        Persons = new(
            Persons.OrderBy(x => x.firstName).ToList()
            );
    }
    else
    {
        Persons = new(
            Persons.OrderByDescending(x => x.firstName).ToList()
            );
    }
}

Однако для двух кнопок код раздувается уже до следующего:

private void UpdateSorting()
{
    if (Buttons[0].IsSortedAscending)
    {
        if (Buttons[1].IsSortedAscending)
        {
            Persons = new(
                Persons
                    .OrderBy(x => x.firstName)
                    .ThenBy(x => x.lastName)
                    .ToList()
            );
        }
        else
        {
            Persons = new(
                Persons
                    .OrderBy(x => x.firstName)
                    .ThenByDescending(x => x.lastName)
                    .ToList()
            );
        }
    }
    else
    {
        if (Buttons[1].IsSortedAscending)
        {
            Persons = new(
                Persons
                    .OrderByDescending(x => x.firstName)
                    .ThenBy(x => x.lastName)
                    .ToList()
            );
        }
        else
        {
            Persons = new(
                Persons
                    .OrderByDescending(x => x.firstName)
                    .ThenByDescending(x => x.lastName)
                    .ToList()
            );
        }
    }
}

С каждой дополнительной кнопкой количество необходимых операторов if/else удваивается: это не может быть идиоматическим или лучшим решением.

Возможно, вы можете использовать System.Linq.Dynamic

Magnus 17.06.2024 10:22

Непонятно, что именно вы хотите сделать. Можете ли вы показать несколько примеров кода, который вы хотите написать, используя «одну чистую функцию», которую вы пытаетесь создать?

Sweeper 17.06.2024 10:37

Каждая операция LINQ возвращает новый запрос. Вы можете добавить Where, OrderBy или ThenBy по мере необходимости, в зависимости от параметров. Не обязательно указывать всех операторов сразу. Вы можете написать, например if (somParam) { query=query.Where();} if (someOrder){ query=query.OrderBy(...);}

Panagiotis Kanavos 17.06.2024 11:49

Другая оптимизация — сохранить Expression<Func<TSource,TKey>>, необходимый для OrderBy, как свойство ButtonObject и просто использовать его для построения запроса. Код сортировки может просто перебирать кнопки и применять правильный оператор и функцию, не зная, какое поле используется для сортировки.

Panagiotis Kanavos 17.06.2024 11:55
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
61
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы можете попробовать этот код:

public static IOrderedEnumerable<T> DynamicSort<T>(this IEnumerable<T> input, params (Func<T,object> selector, SortOrder sortOrder)[] parameters)
{
    IOrderedEnumerable<T> result = null;
    foreach(var p in parameters)
    {
        if (result == null)
        {
            result = p.sortOrder == SortOrder.Asc ? input.OrderBy(p.selector) : input.OrderByDescending(p.selector);
        }
        else
        {
            result = p.sortOrder == SortOrder.Asc ? result.ThenBy(p.selector) : result.ThenByDescending(p.selector);
        }
    }
    return result;
}

Вы вызываете этот метод, например. к

persons.DynamicSort(
     (new Func<Person,object>(x => x.firstName), SortOrder.Asc),
     (new Func<Person,object>(x => x.lastName), SortOrder.Desc),
     (new Func<Person,object>(x => x.DOT), SortOrder.Asc));

Это будет преобразовано в запрос LINQ

persons.OrderBy(x => x.firstName).ThenByDescending(x => x.lastName).ThenBy(x => x.DIT);

Этот метод к настоящему времени полностью разработан. Вам следует добавить несколько проверок, имеет ли ввод значение NULL или в массиве нет элементов. Более того, поскольку методы селектора имеют объект в качестве возвращаемого типа, типы значений будут заключены в рамку. Это означает, что при сборке мусора требуется больше работы, и это снижает производительность. Но, возможно, это хорошая отправная точка для вас.

Онлайн-демо: https://dotnetfiddle.net/6neHxr

Мне кажется, все в порядке; аргумент params должен быть заполнен из графического интерфейса.

Codor 17.06.2024 11:25

Это работает только с IEnumerable, а не с IQueryable, и даже там это приведет к упаковке типов значений. Это не удастся с EF или любым другим поставщиком, кроме LINQ-to-Objects. Исправить это легко, используйте IQueryable вместо IEnumerable, НЕ используйте object где угодно и используйте Expression<Func<TSource,TKey>>

Panagiotis Kanavos 17.06.2024 11:49

@PanagiotisKanavos Сначала я подумал о Func<TSource,TKey>. Но если вы хотите упорядочить строку, а затем целое число, это не сработает. Вот почему я выбрал объект в качестве возвращаемого типа, несмотря на его недостаток. Кроме того, из примера ОП было ясно, что он хочет отсортировать ObservableCollection, который хранится локально в памяти и не использует EF.

SomeBody 17.06.2024 11:51

Другие вопросы по теме