Форум программистов, компьютерный форум, киберфорум
C#: WPF, UWP и Silverlight
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  
 
 
Рейтинг 5.00/14: Рейтинг темы: голосов - 14, средняя оценка - 5.00
 Аватар для INexteR
17 / 16 / 1
Регистрация: 25.01.2023
Сообщений: 468
WPF

Отправка объекта IList с выбранными элементами в ViewModel

25.01.2023, 19:19. Показов 4140. Ответов 69

Студворк — интернет-сервис помощи студентам
Здравствуйте!

Допустимо ли в контексте MVVM следующим образом пробросить в ViewModel объект выбранных в списке элементов? Нарушает ли данная операция идею шаблона?
Представление:
XML
1
2
3
<Grid>
    <ListBox x:Name="lb" />
</Grid>
Застраничный код представления:
C#
1
2
3
4
5
6
public ProductsView()
{
    InitializeComponent();
    lb.SetBinding(TagProperty, new Binding { Path = new("SelectedProducts"), Mode = BindingMode.OneWayToSource });
    lb.SetCurrentValue(TagProperty, lb.SelectedItems);
}
Модель представления:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ProductsViewModel : ViewModelBase
{
    private IList _selectedProducts = null!;
    public IList SelectedProducts
    {
        set
        {
            if (_selectedProducts is null)
            {
                _selectedProducts = value;
            }
            else
                throw new InvalidOperationException($"{nameof(SelectedProducts)} has already been set");
        }
    }
}
0
Лучшие ответы (1)
Programming
Эксперт
39485 / 9562 / 3019
Регистрация: 12.04.2006
Сообщений: 41,671
Блог
25.01.2023, 19:19
Ответы с готовыми решениями:

Как сделать чтобы обработчик события работал со всеми элементами выбранными .querySelectorAll()?
var a=document.querySelectorAll('.portfolio img'); // получаем картинки с HTML; a.onclick=function(){ a.style.display='none'; }...

Передача объекта в ViewModel
Добрый день! Пробую написать для себя простое приложение под Android используя VS + xamarin + MvvmCross. Основной экран отображает...

Передача объекта во View из ViewModel (WPF/MVVM/Caliburn)
Всем привет! Осваиваюсь с WPF и столкнулся со следующей проблемой: Есть ViewModel: public class CategoriesViewModel :...

69
 Аватар для INexteR
17 / 16 / 1
Регистрация: 25.01.2023
Сообщений: 468
28.01.2023, 18:40  [ТС]
Студворк — интернет-сервис помощи студентам
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Поэтому наследование от ViewModelBase уберите.
Да, я вначале написал, но потом понял, что это не нужно, но забыл убрать.
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Я буду писать все замечания, а не только по основному вопросу.
1) Класс ProductProxy иммутабельный. По сути это DTO. Реализация INPC в нём не нужна. Поэтому наследование от ViewModelBase уберите.
Если нужна возможность смены отражаемой сущности (а это ОЧЕНЬ часто бывает нужно), то советую такую реализацию/
Добавить в ViewModelBase:
Насколько я понимаю, PropertyChanged с передачей пустой строки говорит о том, что изменились все свойства?
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Но тогда зачем её декапсулировать public Product? Product => data;?
Ну, ранее я обзывал все вот эти обертывающие классы для одиночного объекта по принципу SomeNameViewModel. Считал, что они считаются составляющими VM. А VM, как мне известно, должна предоставлять свойства в удобном для отображения виде. Но, как выяснилось, эти классы вовсе никакие не VM, а называются прокси
0
1596 / 601 / 185
Регистрация: 05.12.2015
Сообщений: 970
28.01.2023, 18:48
INexteR, Переделал твой пример. Фильтрацию надо обновлять через свойства, а не методы. Там установлен Fody.


UnholyImplementation.zip
1
 Аватар для INexteR
17 / 16 / 1
Регистрация: 25.01.2023
Сообщений: 468
28.01.2023, 18:50  [ТС]
Элд Хасп, подождите, Вы пишите про поддержку возможности смены сущности, но как я могу заменить объект одного типа в Proxy объектом другого типа? Или эти примеры не связаны друг с другом?
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16166 / 11286 / 2892
Регистрация: 21.04.2018
Сообщений: 33,177
Записей в блоге: 2
28.01.2023, 19:10
INexteR, чуть отвлекли домашние дела.
По основному вопросу.

Основа кастомной фильтрации, сортировки такая группа типов:
C#
1
2
3
4
5
6
7
8
9
namespace UnholyImplementation.View
{
    public enum SortDirection
    {
        None = 0,
        Ascending = 1,
        Descending = 2
    }
}
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
namespace UnholyImplementation.View
{
    public readonly struct DiscountRange : IEquatable<decimal?>
    {
        public decimal Min { get; }
        public decimal Max { get; }
 
        public DiscountRange(decimal min, decimal max)
        {
            if (min > max)
                (min, max) = (max, min);
            Min = min;
            Max = max;
        }
 
        public bool Equals(decimal? other)
        {
            decimal num = other ?? 0;
            return num >= Min && num <= Max;
        }
 
        public override string? ToString()
        {
            if (Max == decimal.MaxValue)
                return $"{Min:P2} и более";
            return $"{Min:P2}-{Max:P2}";
        }
    }
}
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
namespace UnholyImplementation.View
{
    public class ProductsViewSource : CollectionViewSource
    {
        public SortDirection Direction
        {
            get => (SortDirection)GetValue(DirectionProperty);
            set => SetValue(DirectionProperty, value);
        }
 
        // Using a DependencyProperty as the backing store for Direction.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DirectionProperty =
            DependencyProperty.Register(
                nameof(Direction),
                typeof(SortDirection),
                typeof(ProductsViewSource),
                new PropertyMetadata(SortDirection.None, OnDirectionChanged));
 
 
 
        public DiscountRange? DiscountRange
        {
            get => (DiscountRange?)GetValue(DiscountRangeProperty);
            set => SetValue(DiscountRangeProperty, value);
        }
 
        // Using a DependencyProperty as the backing store for DiscountRange.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DiscountRangeProperty =
            DependencyProperty.Register(
                nameof(DiscountRange),
                typeof(DiscountRange?),
                typeof(ProductsViewSource),
                new PropertyMetadata(null, (d, e) =>
                {
                    ProductsViewSource source = CheckSource(d);
 
                    source.range = (DiscountRange?)e.NewValue;
                    ChangeFilter(source);
                }));
 
 
 
        public string SubName
        {
            get { return (string)GetValue(SubNameProperty); }
            set { SetValue(SubNameProperty, value); }
        }
 
        // Using a DependencyProperty as the backing store for SubName.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SubNameProperty =
            DependencyProperty.Register(
                "SubName",
                typeof(string),
                typeof(ProductsViewSource),
                new PropertyMetadata(string.Empty, (d, e) =>
                {
                    ProductsViewSource source = CheckSource(d);
 
                    string subName = ((string)e.NewValue ?? string.Empty).Trim();
                    if (!string.Equals(source.subName, subName, StringComparison.OrdinalIgnoreCase))
                    {
                        source.subName = subName;
                        ChangeFilter(source);
                    }
                },
                    (_, baseValue) => baseValue ?? string.Empty));
 
 
 
        private static ProductsViewSource CheckSource(DependencyObject dObj)
        {
            if (dObj is not ProductsViewSource source)
                throw new NotImplementedException($"Реализовано только для {typeof(ProductsViewSource).FullName}.");
            return source;
        }
        private static void OnDirectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ProductsViewSource source = CheckSource(d);
 
            source.SortDescriptions.Clear();
            SortDirection direction = (SortDirection)e.NewValue;
 
            if (direction is SortDirection.Ascending or SortDirection.Descending)
            {
                SortDescription description = new SortDescription
                (
                    nameof(ProductProxy.Cost),
                    direction == SortDirection.Ascending ? ListSortDirection.Ascending : ListSortDirection.Descending
                );
                source.SortDescriptions.Add(description);
            }
        }
        private DiscountRange? range;
        private string subName = string.Empty;
        private FilterEventHandler? rangeSubNameFilter;
 
        private static void ChangeFilter(ProductsViewSource source)
        {
            source.Filter -= source.rangeSubNameFilter;
            if (source.range is DiscountRange range)
            {
                if (string.IsNullOrWhiteSpace(source.subName))
                {
                    source.rangeSubNameFilter = (_, e) =>
                    {
                        ProductProxy item = (ProductProxy)e.Item;
                        e.Accepted = range.Equals(item.Discount);
                    };
                }
                else
                {
                    source.rangeSubNameFilter = (_, e) =>
                    {
                        ProductProxy item = (ProductProxy)e.Item;
                        e.Accepted = range.Equals(item.Discount) &&
                        item.Name.Contains(source.subName, StringComparison.OrdinalIgnoreCase);
                    };
 
                }
            }
            else
            {
                if (string.IsNullOrWhiteSpace(source.subName))
                {
                    source.rangeSubNameFilter = null;
                }
                else
                {
                    source.rangeSubNameFilter = (_, e) =>
                    {
                        ProductProxy item = (ProductProxy)e.Item;
                        e.Accepted = item.Name.Contains(source.subName, StringComparison.OrdinalIgnoreCase);
                    };
 
                }
 
            }
            source.Filter += source.rangeSubNameFilter;
        }
 
        public ProductsViewSource()
        {
            IsLiveFilteringRequested = true;
            IsLiveSortingRequested = true;
            LiveFilteringProperties.Add(nameof(ProductProxy.Name));
            LiveFilteringProperties.Add(nameof(ProductProxy.Discount));
            LiveSortingProperties.Add(nameof(ProductProxy.Cost));
        }
    }
}
Соответственно из VM выкидывается всё несвязанное с отражением Модели :
C#
1
2
3
4
5
6
7
8
namespace UnholyImplementation.ViewModels
{
    public class NewMainViewModel : ViewModelBase
    {
        public ObservableCollection<ProductProxy> Products { get; }
            = new(Source.Products.Select(p => new ProductProxy(p)));
    }
}
Теперь Окно.
Сначала хелпер:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnholyImplementation.View;
 
namespace UnholyImplementation
{
    public static class WinHelper
    {
        public static ReadOnlyCollection<object> DiscountRanges { get; } = Array.AsReadOnly(new object[]
        {
            "Все диапазоны",
            new DiscountRange(0, 0.0999m),
            new DiscountRange(0.10m, 0.1499m),
            new DiscountRange(0.15m, decimal.MaxValue)
        });
 
        public static ReadOnlyCollection<SortDirection> Directions { get; } = Array.AsReadOnly(Enum.GetValues<SortDirection>());
    }
}
И XAML:
XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
<Window x:Class="UnholyImplementation.NewMainWindow"
        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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:UnholyImplementation"
        xmlns:view="clr-namespace:UnholyImplementation.View"
        xmlns:vms="clr-namespace:UnholyImplementation.ViewModels"
        mc:Ignorable="d"
        Title="MainView"
        MinHeight="450"
        MinWidth="800"
        WindowStartupLocation="CenterScreen"
        FontSize="18"
        DataContext="{DynamicResource mainVM}">
    <Window.Resources>
        <vms:NewMainViewModel x:Key="mainVM"/>
        <view:ProductsViewSource x:Key="products"
                                 Source="{Binding Products}"
                                 SubName="{Binding Text, ElementName=subName}"
                                 DiscountRange="{Binding SelectedItem, ElementName=ranges}"
                                 Direction="{Binding SelectedItem, ElementName=directions}"/>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ListBox ItemsSource="{Binding Mode=OneWay, Source={StaticResource products}}"
                 ScrollViewer.CanContentScroll="False"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                 SelectionMode="Multiple">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border BorderThickness="1"
                            BorderBrush="Black">
                        <StackPanel Margin="7">
                            <TextBlock>
                                <Run Text="Название:" />
                                <Run Text="{Binding Name, Mode=OneTime}" />
                            </TextBlock>
                            <TextBlock>
                                <Run Text="Цена:" />
                                <Run Text="{Binding Cost, Mode=OneTime}" />
                                <Run Text="РУБ."/>
                            </TextBlock>
                            <TextBlock>
                                <Run Text="Скидка:" />
                                <Run Text="{Binding Discount, Mode=OneTime, StringFormat=P0}" />
                            </TextBlock>
                        </StackPanel>
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
            <ListBox.ItemContainerStyle>
                <Style TargetType="ListBoxItem">
                    <Setter Property="HorizontalContentAlignment"
                            Value="Stretch" />
                    <Setter Property="Padding"
                            Value="0" />
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>
        <Grid Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <TextBlock Text="Сортировка по стоимости"
                           HorizontalAlignment="Center" />
                <StackPanel Grid.Row="1"
                            TextBlock.FontSize="11"
                            HorizontalAlignment="Center"
                            VerticalAlignment="Center">
                    <StackPanel.Resources>
                        <DataTemplate DataType="{x:Type view:SortDirection}">
                            <RadioButton IsChecked="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}">
                                <RadioButton.LayoutTransform>
                                    <ScaleTransform ScaleX="1.7"
                                                    ScaleY="1.7" />
                                </RadioButton.LayoutTransform>
                                <RadioButton.Style>
                                    <Style TargetType="RadioButton">
                                        <Setter Property="Content" Value="Без сортировки"/>
                                        <Style.Triggers>
                                            <DataTrigger Binding="{Binding}" Value="{x:Static view:SortDirection.Ascending}">
                                                <Setter Property="Content" Value="По возрастанию"/>
                                            </DataTrigger>
                                            <DataTrigger Binding="{Binding}" Value="{x:Static view:SortDirection.Descending}">
                                                <Setter Property="Content" Value="По убыванию"/>
                                            </DataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </RadioButton.Style>
                            </RadioButton>
                        </DataTemplate>
                    </StackPanel.Resources>
                    <ListBox x:Name="directions" ItemsSource="{x:Static local:WinHelper.Directions}"/>
                </StackPanel>
            </Grid>
            <Grid Column="1">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <TextBlock Text="Фильтрация по размеру скидки"
                           HorizontalAlignment="Center" />
                <ComboBox Grid.Row="1"
                          VerticalAlignment="Center"
                          x:Name="ranges"
                          ItemsSource="{x:Static local:WinHelper.DiscountRanges}"
                          SelectedIndex="0"/>
            </Grid>
            <Grid Column="2">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <TextBlock Text="Поиск по наименованию"
                           HorizontalAlignment="Center" />
                <TextBox x:Name="subName" Grid.Row="1"
                         Text="{Binding Search, UpdateSourceTrigger=PropertyChanged}"
                         VerticalAlignment="Center" />
            </Grid>
        </Grid>
    </Grid>
</Window>
Архив с исходниками: UnholyImplementation.7z
1
 Аватар для INexteR
17 / 16 / 1
Регистрация: 25.01.2023
Сообщений: 468
28.01.2023, 19:16  [ТС]
Элд Хасп, и да, в Proxy ссылка на декапсулированную сущность была для того, чтобы извлекать её и работать именно с ней. В том же моем примере фильтрации или поиска мог бы понадобиться сам объект
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16166 / 11286 / 2892
Регистрация: 21.04.2018
Сообщений: 33,177
Записей в блоге: 2
28.01.2023, 19:27
Цитата Сообщение от INexteR Посмотреть сообщение
PropertyChanged с передачей пустой строки говорит о том, что изменились все свойства?
Да.
Из документации:
The PropertyChanged event can indicate all properties on the object have changed by using either null or String.Empty as the property name in the PropertyChangedEventArgs. Note that in a UWP application, String.Empty must be used rather than null.
Цитата Сообщение от INexteR Посмотреть сообщение
Но, как выяснилось, эти классы вовсе никакие не VM, а называются прокси
Прокси - это по функционалу. То есть в объекте нет собственных данных, он просто отражает данные какого-то другого объекта или объектов. Так же как, например, прокси в сети (интернете) - он просто маршутизирует запросы и ответы на них.
В этом смысле вся VM (весь слой) - это прокси Модели.
Поэтому, то что эти классы прокси, это не значит что они не часть ViewModel.

Добавлено через 1 минуту
По использованию эти классы вполне можно назвать "маленькими вээмками".

Добавлено через 7 минут
Цитата Сообщение от INexteR Посмотреть сообщение
Proxy ссылка на декапсулированную сущность была для того, чтобы извлекать её и работать именно с ней.
Так как это мутабельный класс, то лучше так не делать.
Если бы был иммутабельный DTO, то было бы вполне допустимо.

Цитата Сообщение от INexteR Посмотреть сообщение
Вы пишите про поддержку возможности смены сущности, но как я могу заменить объект одного типа в Proxy объектом другого типа?
Не про тип речь.
Допустим, изменяется существующий Product.
Из Модели приходит новый Product с новыми значениями в свойствах.
Вам нужно "гладко" заменить старый Product новым.
Как это сделать?
Он может быть в разных коллекциях, в разных свойствах, в разных окнах и т.д.
Искать во всех возможных места?
Обычно делается таблица (или словарь) соответствий между Product и его прокси, который используется в VM и View.
Непосредственно Product нигде не используется.
Как пришёл новый новый Product, по таблице ищется его прокси и только в нём заменяется старый.
Так как при этом происходит уведомление по всем свойствам, то автоматически обновляется всё представление где бы не использовался этот прокси.
1
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16166 / 11286 / 2892
Регистрация: 21.04.2018
Сообщений: 33,177
Записей в блоге: 2
28.01.2023, 21:07
INexteR, чуть изменил реализацию.
Так, по-моему, лучше читается код:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
namespace UnholyImplementation.View
{
    public class ProductsViewSource : CollectionViewSource
    {
        public SortDirection Direction
        {
            get => (SortDirection)GetValue(DirectionProperty);
            set => SetValue(DirectionProperty, value);
        }
 
        // Using a DependencyProperty as the backing store for Direction.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DirectionProperty =
            DependencyProperty.Register(
                nameof(Direction),
                typeof(SortDirection),
                typeof(ProductsViewSource),
                new PropertyMetadata(SortDirection.None, OnDirectionChanged));
 
 
 
        public DiscountRange? DiscountRange
        {
            get => (DiscountRange?)GetValue(DiscountRangeProperty);
            set => SetValue(DiscountRangeProperty, value);
        }
 
        // Using a DependencyProperty as the backing store for DiscountRange.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DiscountRangeProperty =
            DependencyProperty.Register(
                nameof(DiscountRange),
                typeof(DiscountRange?),
                typeof(ProductsViewSource),
                new PropertyMetadata(null, OnRangeChanged));
 
        private static void OnRangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ProductsViewSource source = CheckSource(d);
            source.range = (DiscountRange?)e.NewValue;
 
            source.Filter -= source.rangeFilter;
 
            if (source.range is DiscountRange range)
            {
                source.rangeFilter = (_, e) =>
                {
                    ProductProxy item = (ProductProxy)e.Item;
                    e.Accepted &= range.Equals(item.Discount);
                };
            }
            else
            {
                source.rangeFilter = null;
            }
 
            source.Filter += source.rangeFilter;
        }
 
        public string SubName
        {
            get { return (string)GetValue(SubNameProperty); }
            set { SetValue(SubNameProperty, value); }
        }
 
        // Using a DependencyProperty as the backing store for SubName.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SubNameProperty =
            DependencyProperty.Register(
                "SubName",
                typeof(string),
                typeof(ProductsViewSource),
                new PropertyMetadata(string.Empty, OnSubNameChanged, (_, baseValue) => baseValue ?? string.Empty));
 
        private static void OnSubNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ProductsViewSource source = CheckSource(d);
            string subName = ((string)e.NewValue ?? string.Empty).Trim();
            if (string.Equals(source.subName, subName, StringComparison.OrdinalIgnoreCase))
                return;
            source.subName = subName;
 
            source.Filter -= source.subNameFilter;
 
            if (string.IsNullOrWhiteSpace(subName))
            {
                source.subNameFilter = null;
            }
            else
            {
                source.subNameFilter = (_, e) =>
                {
                    ProductProxy item = (ProductProxy)e.Item;
                    e.Accepted &= item.Name.Contains(subName, StringComparison.OrdinalIgnoreCase);
                };
 
            }
 
            source.Filter += source.subNameFilter;
        }
 
 
        private static ProductsViewSource CheckSource(DependencyObject dObj)
        {
            if (dObj is not ProductsViewSource source)
                throw new NotImplementedException($"Реализовано только для {typeof(ProductsViewSource).FullName}.");
            return source;
        }
        private static void OnDirectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ProductsViewSource source = CheckSource(d);
 
            source.SortDescriptions.Clear();
            SortDirection direction = (SortDirection)e.NewValue;
 
            if (direction is SortDirection.Ascending or SortDirection.Descending)
            {
                SortDescription description = new SortDescription
                (
                    nameof(ProductProxy.Cost),
                    direction == SortDirection.Ascending ? ListSortDirection.Ascending : ListSortDirection.Descending
                );
                source.SortDescriptions.Add(description);
            }
        }
        private DiscountRange? range;
        private string subName = string.Empty;
        private FilterEventHandler? rangeFilter;
        private FilterEventHandler? subNameFilter;
        public ProductsViewSource()
        {
            IsLiveFilteringRequested = true;
            IsLiveSortingRequested = true;
            LiveFilteringProperties.Add(nameof(ProductProxy.Name));
            LiveFilteringProperties.Add(nameof(ProductProxy.Discount));
            LiveSortingProperties.Add(nameof(ProductProxy.Cost));
        }
    }
}
0
 Аватар для INexteR
17 / 16 / 1
Регистрация: 25.01.2023
Сообщений: 468
28.01.2023, 21:27  [ТС]
proa33, хороший пример, но я не знаком с таким пакетом как Fody. И ещё такой нюанс: поле выбора радиокнопки сортировку уже нельзя отменить, а в моем примере такая функция поддерживалось посредством щелчка по уже активной кнопке

Добавлено через 12 минут
Элд Хасп, хорошо, спасибо, но только не понял, каким образом происходит данное
Цитата Сообщение от Элд Хасп Посмотреть сообщение
по таблице ищется его прокси и только в нём заменяется старый
Пришел новый, в таблице есть лишь прокси со старым, как по новому определить необходимый прокси?
0
1596 / 601 / 185
Регистрация: 05.12.2015
Сообщений: 970
28.01.2023, 21:36
Цитата Сообщение от INexteR Посмотреть сообщение
не знаком с таким пакетом как Fody
Он заменяет BaseModel - изучите на этом примере атрибуты
Цитата Сообщение от INexteR Посмотреть сообщение
такая функция поддерживалось посредством щелчка по уже активной кнопке
Так делать нельзя - у кнопок должна быть одна ответственность.
Вот добавил эту кнопку. Сравните сколько это стоило усилий.

UnholyImplementation.zip
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16166 / 11286 / 2892
Регистрация: 21.04.2018
Сообщений: 33,177
Записей в блоге: 2
28.01.2023, 22:52
Цитата Сообщение от INexteR Посмотреть сообщение
хороший пример, но я не знаком с таким пакетом как Fody.
Там ничего сложного.
Просто подключаете пакет и он в посткомпиляции заменяет автоствойства на свойства с уведомлением INPC.

Сам Fody - это пакет для посткомпиляционной сборки.
Для INPC нужно использовать PropertyChanged.Fody.
Для команд тоже есть расширение.
Кроме внедрения уведомления можно с помощью атрибутов задавать зависимости свойств.
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16166 / 11286 / 2892
Регистрация: 21.04.2018
Сообщений: 33,177
Записей в блоге: 2
29.01.2023, 00:15
Цитата Сообщение от INexteR Посмотреть сообщение
Пришел новый, в таблице есть лишь прокси со старым, как по новому определить необходимый прокси?
Пример Модели с изменяющимся состоянием:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
 
namespace UnholyImplementation.Model
{
    public static class Source
    {
        private readonly static List<Product> _products;
 
        static Source()
        {
            _products = new List<Product> //условная бд
            {
                new Product{ Name = "Блюдо", Cost = 1890, Discount = 43 },
                new Product{ Name = "Блюдо", Cost = 690, Discount = 2 },
                new Product{ Name = "Диффузор", Cost = 800, Discount = 32 },
                new Product{ Name = "Декор настенный", Cost = 1790, Discount = 30 },
                new Product{ Name = "Диффузор", Cost = 600, Discount = 2 },
                new Product{ Name = "Растение", Cost = 1000, Discount = null },
                new Product{ Name = "Диффузор", Cost = 500, Discount = 11 },
                new Product{ Name = "Фоторамка", Cost = 999, Discount = 2 },
                new Product{ Name = "Фоторамка", Cost = 600, Discount = 12 },
                new Product{ Name = "Фоторамка", Cost = 1590, Discount = 25 },
                new Product{ Name = "Кашпо", Cost = 1400, Discount = 4 },
                new Product{ Name = "Фоторамка", Cost = 359, Discount = 4 },
                new Product{ Name = "Часы", Cost = 5600, Discount = 25 },
                new Product{ Name = "Часы", Cost = 999, Discount = 3 },
                new Product{ Name = "Аромат", Cost = 2790, Discount = 2 },
                new Product{ Name = "Аромат", Cost = 3500, Discount = 4 },
                new Product{ Name = "Аромат", Cost = 7900, Discount = 21 },
                new Product{ Name = "Подставка", Cost = 1400, Discount = 3 },
                new Product{ Name = "Подставка", Cost = 4000, Discount = 22 },
                new Product{ Name = "Подставка", Cost = 3590, Discount = 13 },
                new Product{ Name = "Шкатулка", Cost = 8000, Discount = 5 },
                new Product{ Name = "Шкатулка", Cost = 690, Discount = 2 },
                new Product{ Name = "Свеча", Cost = 250, Discount = 52 },
                new Product{ Name = "Свеча", Cost = 1000, Discount = null },
                new Product{ Name = "Свеча", Cost = 299, Discount = 13 },
                new Product{ Name = "Подсвечник", Cost = 2600, Discount = 3 },
                new Product{ Name = "Ваза", Cost = 1990, Discount = 3 },
                new Product{ Name = "Ваза", Cost = 100, Discount = 10 },
                new Product{ Name = "Ваза", Cost = 1300, Discount = 28 },
                new Product{ Name = "Ваза", Cost = 1999, Discount = 18 }          
            };
 
            for (int i = 0; i < _products.Count; )
            {
                _products[i].Id = ++i;
            }
            Task.Run(ChangeProduct);
        }
 
        public static IEnumerable<Product> Products => _products;
 
        public static event EventHandler<Product>? ProductChanged;
 
        private static readonly Random random = new Random();
C#
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
        private static async void ChangeProduct()
        {
            while (true)
            {
                await Task.Delay(500);
                int index = random.Next(_products.Count);
                decimal cost = random.Next(10_000);
                sbyte discount = (sbyte) random.Next(26);
                Product product= _products[index];
                product.Cost = cost;
                product.Discount = discount;
                ProductChanged?.Invoke(null, product);
            }
        }
    }
}
Прокси:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using System.Threading.Tasks;
using UnholyImplementation.Model;
 
namespace UnholyImplementation.ViewModels
{
    public class ProductProxy : Proxy<Product>
    {
 
        public int Id { get; }
        //public Product? Product => data;
 
        public string Name => data?.Name ?? string.Empty;
 
        public decimal Cost => data?.Cost ?? 0;
 
        public decimal Discount => (data?.Discount ?? 0) * 0.01m;
 
        private bool _isChanged;
        public bool IsChanged { get => _isChanged; private set => Set(ref _isChanged, value); }
        public ProductProxy(Product product)
            : base(product)
        {
            Id = product.Id;
        }
 
        protected override void OnSetData(Product newData)
        {
            base.OnSetData(newData);
 
            IsChanged = true;
            Task.Run(async () => { await Task.Delay(5_000); IsChanged = false; });
        }
    }
}
ViewModel:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using UnholyImplementation.Model;
 
namespace UnholyImplementation.ViewModels
{
    public class NewMainViewModel : ViewModelBase
    {
        public ObservableCollection<ProductProxy> Products { get; }
            = new(Source.Products.Select(p => new ProductProxy(p)));
 
        private readonly Dictionary<int, ProductProxy> _products;
 
        public NewMainViewModel()
        {
            _products = Products.ToDictionary(p => p.Id);
            Source.ProductChanged += OnProductChanged;
        }
 
        private void OnProductChanged(object? sender, Product product)
        {
            _products[product.Id].SetData(product);
        }
    }
}
Окно:
XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
<Window x:Class="UnholyImplementation.NewMainWindow"
        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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:UnholyImplementation"
        xmlns:view="clr-namespace:UnholyImplementation.View"
        xmlns:vms="clr-namespace:UnholyImplementation.ViewModels"
        mc:Ignorable="d"
        Title="MainView"
        MinHeight="450"
        MinWidth="800" Width="1600"
        WindowStartupLocation="CenterScreen"
        FontSize="18"
        DataContext="{DynamicResource mainVM}">
    <Window.Resources>
        <vms:NewMainViewModel x:Key="mainVM"/>
        <view:ProductsViewSource x:Key="products"
                                 Source="{Binding Products}"
                                 SubName="{Binding Text, ElementName=subName}"
                                 DiscountRange="{Binding SelectedItem, ElementName=ranges}"
                                 Direction="{Binding SelectedItem, ElementName=directions}"/>
        
        <DataTemplate DataType="{x:Type vms:ProductProxy}">
            <Border x:Name="PART_Main" BorderThickness="1"
                            BorderBrush="Black"
                            Margin="2" Background="AliceBlue">
                <StackPanel Margin="7">
                    <TextBlock Text="{Binding Name, StringFormat='Название: {0}'}"/>
                    <TextBlock Text="{Binding Cost, StringFormat='Цена: {0} РУБ.'}"/>
                    <TextBlock Text="{Binding Discount, StringFormat='Скидка: {0:P0}'}"/>
                </StackPanel>
            </Border>
            <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding IsChanged}" Value="True">
                    <Setter TargetName="PART_Main" Property="Background" Value="Coral"/>
                </DataTrigger>
            </DataTemplate.Triggers>
        </DataTemplate>
 
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ListBox x:Name="productsLBox" ItemsSource="{Binding Source={StaticResource products}}"
                 ScrollViewer.CanContentScroll="False"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                 SelectedIndex="0">
            <ListBox.ItemContainerStyle>
                <Style TargetType="ListBoxItem">
                    <Setter Property="HorizontalContentAlignment"
                            Value="Stretch" />
                    <Setter Property="Padding"
                            Value="0" />
                </Style>
            </ListBox.ItemContainerStyle>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ListBox>
        <Grid Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <Grid Margin="5">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <TextBlock Text="Сортировка по стоимости"
                           HorizontalAlignment="Center" />
                <StackPanel Grid.Row="1"
                            TextBlock.FontSize="11"
                            HorizontalAlignment="Center"
                            VerticalAlignment="Center">
                    <StackPanel.Resources>
                        <DataTemplate DataType="{x:Type view:SortDirection}">
                            <RadioButton IsChecked="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}">
                                <RadioButton.LayoutTransform>
                                    <ScaleTransform ScaleX="1.7"
                                                    ScaleY="1.7" />
                                </RadioButton.LayoutTransform>
                                <RadioButton.Style>
                                    <Style TargetType="RadioButton">
                                        <Setter Property="Content" Value="Без сортировки"/>
                                        <Style.Triggers>
                                            <DataTrigger Binding="{Binding}" Value="{x:Static view:SortDirection.Ascending}">
                                                <Setter Property="Content" Value="По возрастанию"/>
                                            </DataTrigger>
                                            <DataTrigger Binding="{Binding}" Value="{x:Static view:SortDirection.Descending}">
                                                <Setter Property="Content" Value="По убыванию"/>
                                            </DataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </RadioButton.Style>
                            </RadioButton>
                        </DataTemplate>
                    </StackPanel.Resources>
                    <ListBox x:Name="directions" ItemsSource="{x:Static local:WinHelper.Directions}"/>
                </StackPanel>
            </Grid>
            <Grid Column="1" Margin="5">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <TextBlock Text="Фильтрация по размеру скидки"
                           HorizontalAlignment="Center" />
                <ComboBox Grid.Row="1"
                          VerticalAlignment="Center"
                          x:Name="ranges"
                          ItemsSource="{x:Static local:WinHelper.DiscountRanges}"
                          SelectedIndex="0"/>
            </Grid>
            <Grid Column="2" Margin="5">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <TextBlock Text="Поиск по наименованию"
                           HorizontalAlignment="Center" />
                <TextBox x:Name="subName" Grid.Row="1"
                         Text="{Binding Search, UpdateSourceTrigger=PropertyChanged}"
                         VerticalAlignment="Center" />
            </Grid>
            <ContentControl Grid.Column="3" Margin="5"
                            Content="{Binding SelectedItem, ElementName=productsLBox}"/>
        </Grid>
    </Grid>
</Window>


Исходники: UnholyImplementationV01.7z
1
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16166 / 11286 / 2892
Регистрация: 21.04.2018
Сообщений: 33,177
Записей в блоге: 2
29.01.2023, 00:25
INexteR, вот с таким шаблоном красивше:
XML
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
        <DataTemplate DataType="{x:Type vms:ProductProxy}">
            <Border x:Name="PART_Main" BorderThickness="1"
                            BorderBrush="Black"
                            Margin="2" Background="AliceBlue">
                <StackPanel Margin="7">
                    <TextBlock Text="{Binding Name, StringFormat='Название: {0}'}"/>
                    <TextBlock Text="{Binding Cost, StringFormat='Цена: {0} РУБ.'}"/>
                    <TextBlock Text="{Binding Discount, StringFormat='Скидка: {0:P0}'}"/>
                </StackPanel>
            </Border>
            <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding IsChanged}" Value="True">
                    <!--<Setter TargetName="PART_Main" Property="Background" Value="Coral"/>-->
                    <DataTrigger.EnterActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <ColorAnimation Duration="0:0:3"
                                                From="Coral"
                                                To="AliceBlue"
                                                Storyboard.TargetName="PART_Main"
                                                Storyboard.TargetProperty="Background.Color"/>
                            </Storyboard>
                        </BeginStoryboard>
                    </DataTrigger.EnterActions>
                </DataTrigger>
            </DataTemplate.Triggers>
        </DataTemplate>
1
 Аватар для INexteR
17 / 16 / 1
Регистрация: 25.01.2023
Сообщений: 468
29.01.2023, 11:30  [ТС]
Цитата Сообщение от Элд Хасп Посмотреть сообщение
C#
1
2
3
4
private void OnProductChanged(object? sender, Product product)
{
     _products[product.Id].SetData(product);
}
А, извлечение старого происходит по уникальному ключу, который не меняется. Я до этого подумал, что изменяются все свойства, в том числе и уникальный идентификатор при наличии

Добавлено через 5 минут
Ну и конечно же спасибо Вам за это гениальное решение, это просто шарм! Надо во всем разобраться.
Ещё увидел для себя некоторую особенность - вызов Task.Run с передачей асинхронного метода. Никогда так не делал, ставил await на самом Task.Run и передавал синхронный метод. Интересный трюк

Добавлено через 18 минут
Цитата Сообщение от proa33 Посмотреть сообщение
Вот добавил эту кнопку. Сравните сколько это стоило усилий.
Ну да, согласен, добавив третью кнопку, нужно прикладывать не так много усилий. И, к тому же, отключение кнопки не является стандартным поведением
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16166 / 11286 / 2892
Регистрация: 21.04.2018
Сообщений: 33,177
Записей в блоге: 2
29.01.2023, 11:48
Цитата Сообщение от INexteR Посмотреть сообщение
вызов Task.Run с передачей асинхронного метода. Никогда так не делал, ставил await на самом Task.Run и передавал синхронный метод.
В моём варианте не нужно ожидание завершения.
По факту я создаю минитаймер.
Но его удобнее отлаживать, так как остановка в коде метода или в обработчике события (по breakpoint) останавливает таймер и можно пошагово проверить как работает код.
С обычным таймером так не выйдет поскольку остановка в одном обработчике, не останавливает поднятие событий для следующих интервалов.

А то о чем вы пишите делается тогда, когда нужно дождаться ожидания выполнения таска.

Добавлено через 14 минут
Цитата Сообщение от INexteR Посмотреть сообщение
Я до этого подумал, что изменяются все свойства, в том числе и уникальный идентификатор при наличии
С Id более типичный сценарий.
Но можно сделать и без него.
Тогда от модели будет поступать старая сущность и та которая её заменяет.

Напишу здесь, поэтому не полный код:
C#
55
       public static event EventHandler<(Product old, Product @new)>? ProductChanged;
C#
68
69
70
71
72
                Product old = _products[index];
                Product @new = old.Clone(); // Клонирование. 
                @new.Cost = cost;
                @new.Discount = discount;
                ProductChanged?.Invoke(null, (old, @new));
C#
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
        public ObservableCollection<ProductProxy> Products { get; }
 
        private readonly Dictionary<Product, ProductProxy> _products;
 
        public NewMainViewModel()
        {
            _products = new Dictionary<Product, ProductProxy>();
            foreach (var product in Source.Products)
            {
                _products.Add(product, new ProductProxy(product));
            }
            Products = new(_products.Values);
            Source.ProductChanged += OnProductChanged;
        }
 
        private void OnProductChanged(object? sender, (Product old, Product @new) tuple)
        {
            ProductProxy proxy = _products[tuple.old];
            _products.Remove(tuple.old);
            proxy.SetData(tuple.@new);
            _products.Add(tuple.@new, proxy);
        }
1
 Аватар для INexteR
17 / 16 / 1
Регистрация: 25.01.2023
Сообщений: 468
29.01.2023, 12:25  [ТС]
Цитата Сообщение от Элд Хасп Посмотреть сообщение
C#
1
private readonly Dictionary<Product, ProductProxy> _products;
Да, я тоже подумал, что, в случае не наличия Id, ключом будет служить сам объект, а значением - посредник с этим объектом.

А нормально то, что у меня класс модели статический? Просто у меня источник данных один
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16166 / 11286 / 2892
Регистрация: 21.04.2018
Сообщений: 33,177
Записей в блоге: 2
29.01.2023, 15:04
Цитата Сообщение от INexteR Посмотреть сообщение
А нормально то, что у меня класс модели статический? Просто у меня источник данных один
Ну, по современным взглядам - это моветон.
Статическими могут быть какие-то службы, сервисы.

В чём смысл паттернов семейства MV*?
В абстракции Представления от Бизнес Логики.
В Шарпе такая абстракция задаётся интерфейсом.
А у статического класса нет реализации интерфейса.
Поэтому при малейших изменениях в БЛ Модели, придётся пересобирать и сборки View и/или VM (контролера, презентера и т.д.).
А это нарушает саму концептуальную идею паттернов MV*.
Поэтому для этих паттернов статическая Модель это уже не просто моветон, а явное нарушение паттерна.

Добавлено через 1 минуту
Структура связей в WPF Решении

Добавлено через 3 минуты
На этой схеме нет прямых зависимостей слоёв друг от друга.
Поэтому можно любой слой поменять не трогая при этом другие слои.
Со статическим классом так не получится.

Статический класс может возвращать экземпляр Модели (или Моделей) с которым VM будет работать через интерфейс.
То есть статический класс может быть фабрикой для получения нужных экземпляров. Регистрация типов, инициализация фабрики будет происходить при старте приложения в App.
1
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16166 / 11286 / 2892
Регистрация: 21.04.2018
Сообщений: 33,177
Записей в блоге: 2
29.01.2023, 16:35
INexteR, не пришло в голову нормального примера для демонстрации автозамены через словарь.
Просто добавил категорию в продукт и стал случайно менять название у категории.
Во всех продуктах название категории в представлении автоматически обновляется.
Без словарей и замены в прокси источника это было бы намного сложнее реализовать.



Исходники: UnholyImplementationV02.7z
1
 Аватар для INexteR
17 / 16 / 1
Регистрация: 25.01.2023
Сообщений: 468
29.01.2023, 17:04  [ТС]
Элд Хасп, я в восторге от вашей реализации, правда смутили два триггера данных на константы перечисления, так как эти значения не будут изменяться. Поэтому я видоизменил части кода в представлении
XML
1
Direction="{Binding SelectedValue, ElementName=directions}" />
XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<ListBox x:Name="directions"
         SelectedValuePath="Value"
         ItemsSource="{x:Static local:WinHelper.Directions}"
         SelectedIndex="0">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <RadioButton Content="{Binding Key}"
                         IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}">
                <RadioButton.LayoutTransform>
                    <ScaleTransform ScaleX="1.7"
                                    ScaleY="1.7" />
                </RadioButton.LayoutTransform>
            </RadioButton>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>
И в хелпере
C#
1
2
3
4
5
6
7
public static ReadOnlyDictionary<string, SortDirection> Directions { get; } =
    new Dictionary<string, SortDirection>
    {              
        ["По умолчанию"] = SortDirection.None,
        ["По возрастанию"] = SortDirection.Ascending,
        ["По убыванию"] = SortDirection.Descending       
    }.AsReadOnly();
Ого, ваш последний пример с изменяющимся состоянием модели анимируется прямо в конструкторе, в нем все переливается!
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16166 / 11286 / 2892
Регистрация: 21.04.2018
Сообщений: 33,177
Записей в блоге: 2
29.01.2023, 18:21
INexteR, в общем виде лучше использовать какую-то типовую реализацию.
Что-то типа DictionaryConverter и DictionaryTypeConverter.

Цитата Сообщение от INexteR Посмотреть сообщение
модели анимируется прямо в конструкторе, в нем все переливается!
При правильной реализации всегда должно быть так.
1
 Аватар для INexteR
17 / 16 / 1
Регистрация: 25.01.2023
Сообщений: 468
29.01.2023, 22:59  [ТС]
Элд Хасп, можно спросить, для чего это?
C#
1
2
3
4
5
IsLiveFilteringRequested = true;
IsLiveSortingRequested = true;
LiveFilteringProperties.Add(nameof(ProductProxy.Name));
LiveFilteringProperties.Add(nameof(ProductProxy.Discount));
LiveSortingProperties.Add(nameof(ProductProxy.Cost));
0
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
inter-admin
Эксперт
29715 / 6470 / 2152
Регистрация: 06.03.2009
Сообщений: 28,500
Блог
29.01.2023, 22:59

MVVM. Получение данных объекта по сети - в model или во viewmodel?
Здравствуйте! Вникаю в паттерн mvvm, прочитал\посмотрел кучу учебных материалов, и если честно, в голове каша уже. Мне понравился своей...

Как из ViewModel UserControl'a изменить свойство в ViewModel главного окна?
Недавно начал попытки создавать приложение WPF с применением MVVM, и у меня возник вопрос, который не даёт мне продвигаться в разработке...

WPF получить доступ из одной viewmodel к другой viewmodel
Собственно есть у меня две viewmodel, mainviewmodel и settingsviewmodel, в mainviewmodel есть вот такая штука private Visibility...

Создать словарь с 5 элементами.Поменять местами первый и последний элемент объекта.Удалить второй элемент объекта
Создать словарь с 5 элементами. Поменять местами первый и последний элемент объекта. Удалить второй элемент объекта. Добавить в конец...

Как преобразовать List<List<string>> в IList<IList<string>>
Добрый день! Возникла задача преобразования листа в интерфейс листа. Не подскажите как грамотно сделать. В общем, код класса такой: ...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
60
Ответ Создать тему
Новые блоги и статьи
Генератор паролей
Maks 29.10.2026
Реализация из решения ниже выполнена исключительно по моей инициативе. Самым главным критерием для меня было создание паролей, в котором отсутствуют знаки: | 1 l I Обработка содержит реквизиты: -. . .
Установка ChatGPT (Codex) на Windows без использования Microsoft Store
Fabien159 28.10.2026
Здравствуйте всем читающим! Опишу процесс установки ChatGPT (Codex) на Windows, если по каким-то причинам у вас не получается устанавить его через Microsoft Store. И так: 1. Скачиваем с. . .
Программное добавление префикса в нумерацию документов
Maks 27.10.2026
Реализация из решения ниже выполнена в самописной конфигурации на БСП (помощь начинающему коллеге). Задача: Программно добавить префикс в нумерацию документов. Решение: В модуле объекта. . .
После закрытия оутлука продолжает висеть процесс в диспетчере задач
Maks 27.10.2026
С недавнего времени в продакшене возникла проблема: при закрытии оутлука в диспетчере задач продолжал висеть процесс outlook. exe. Проблема возникла после установки MS Office 2021 LTSC на машинах, под. . .
Baragoz - DPS/HPS бот для WoW 3.3.5a, многоязычный
AmbA 26.10.2026
Хотите ещё ботов? У меня есть один! А я пока отдохну от него. https:/ / www. cyberforum. ru/ blog_attachment. php?attachmentid=12062&stc=1&d=1793007952 Инструкция внутри. https:/ / youtu. be/ VpfFZSUDGnQ. . .
Особенности gif сгенерированных в Си
russiannick 21.10.2026
Рассмотрим генерацию gif файлов, которые в Си имеют свои особенности. Gif представляет собой графический формат популярный в интернете. Посмотрим на палитру. Gif файл сгенерированный в си отличается. . .
FPS контроллер с анимированными Y-Bot и 3D-звуком miniaudio (Box3D, Recast, BehaviorTree.CPP, ozz-animation, ImGui, OpenGL и SDL3)
8Observer8 18.10.2026
Содержание блога | English Version Полное мультиплатформенное руководство по стартовому проекту с FPS-контроллером игрока, анимированными роботами Y-Bot с 4-слойным блендингом, 3D-физикой Box3D,. . .
Локальный http сервер
Argus19 17.10.2026
Локальный http сервер На работе, на двух локальных компьютерах круглосуточно работают программы опроса приборов с записью в книги Excel,которые раз в сутки записываются на сервер компании. Т. к. . . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru