Форум программистов, компьютерный форум, киберфорум
C# .NET
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  
 
Рейтинг 4.80/5: Рейтинг темы: голосов - 5, средняя оценка - 4.80
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16117 / 11238 / 2887
Регистрация: 21.04.2018
Сообщений: 33,038
Записей в блоге: 2

Платформо-Независимая ViewModel

07.01.2023, 14:19. Показов 1090. Ответов 16
Метки нет (Все метки)

Студворк — интернет-сервис помощи студентам
Хоть вопрос и проистекает напрямую из реализации MVVM и, соответственно, тесно связан с WPF, но данный аспект, как мне кажется, больше относится к Шарпу в целом. Поэтому задаю его в этом разделе.

Преамбула.
Во всех паттернах семейства MV* предполагается реализация Модели независящая от Представления, его типа и даже, вообще, его наличия. Такую Модель лучше всего реализовывать на Standard и потом можно будет использовать в приложении с любым типом GUI: Формы, WPF, UWP, Avalonia, Xamarin и т.д.

В семействе WPF GUI используется паттерн MVVM. Паттерн строго иерархический View -> ViewModel -> Model.
Связь верхнего слоя с нижним - сильная, обратная - слабая (через события и делегаты обратного вызова). Связи через слой - не допускаются ни в каком направлении.
Требования к ViewModel в основном возникают из-за внедрённого в WPF механизма привязок. Привязки возможны только к свойствам объекта источника. Для автообновления представления по привязкам в объекте-ичтонике должны подыматься события интерфейсов INotifyPropertyChanged, INotifyCollectionChanged, IBindingList (редко используется) и ICommnad. Так как Модель, в общем случае, имеет произвольную реализацию, то в ней может не быть свойств вовсе, могут использоваться любые (часто кастомные) события, вся работа строится через методы.
Поэтому, для удобной работы WPF, основная функция ViewModel - это отражение Модели в своих свойствах.

Проблема.
Так всё семейство WPF (UWP, Avalonia, Xamarin и др.) построены очень похоже, то по идее одна и та же VM может работать с любым их этих типов GUI. И в однопоточном приложении это действительно так. Сложности возникают при маршализации событий из других потоков в Представление.

Рассмотрим это на примере WPF vs UWP.
В WPF событие INotifyPropertyChanged.PropertyChanged можно подымать в любом потоке, так как его маршалинг в UI поток встроен в WPF привязки.
Поэтому типичная реализация базовой VM такая (взято из Паттерн MVVM - WPF - Professor Web):
C#
8
9
10
11
12
13
14
15
16
17
    public class Notifier : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged = delegate { };
 
        protected void NotifyPropertyChanged(
            string propertyName)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
И такая реализация работает нормально в любом потоке.

А вот для события ICommand.CanExecuteChanged в кнопках и пунктах меню такого маршилинга нет.
Поэтому простейшая реализация (от kolorotur) не всегда правильно работает:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    private readonly EventHandler _requerySuggested;
 
    /// <summary>Событие извещающее об измении состояния команды</summary>
    public event EventHandler CanExecuteChanged;
 
    /// <summary>Конструктор команды</summary>
    /// <param name="execute">Выполняемый метод команды</param>
    /// <param name="canExecute">Метод разрешающий выполнение команды</param>
    public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
    {
        _onExecute = execute;
        _canExecute = canExecute;
 
        _requerySuggested = (o, e) => Invalidate();
        CommandManager.RequerySuggested += _requerySuggested;
    }
 
    public void Invalidate() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
Если вызывать метод Invalidate() не из UI потока, то будет выкинуто исключение.
Поэтому для WPF лучше использовать такую реализацию (Простые реализации для тем на форуме):
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
namespace Simplified
{
    #region Делегаты для методов WPF команд
    public delegate void ExecuteHandler(object parameter);
    public delegate bool CanExecuteHandler(object parameter);
    #endregion
 
    #region Класс команд - RelayCommand
    /// <summary>Класс реализующий <see cref="ICommand"/>.<br/>
    /// Реализация взята из <see href="https://www.cyberforum.ru/wpf-silverlight/thread2390714-page4.html#post13535649"/>
    /// и дополнена конструктором для методов без параметра.</summary>
    public class RelayCommand : ICommand
    {
        private readonly CanExecuteHandler canExecute;
        private readonly ExecuteHandler execute;
        private readonly EventHandler requerySuggested;
 
        /// <summary>Событие извещающее об изменении состояния команды.</summary>
        public event EventHandler CanExecuteChanged;
 
        /// <summary>Конструктор команды.</summary>
        /// <param name="execute">Выполняемый метод команды.</param>
        /// <param name="canExecute">Метод, возвращающий состояние команды.</param>
        public RelayCommand(ExecuteHandler execute, CanExecuteHandler canExecute = null)
            :this()
        {
            this.execute = execute ?? throw new ArgumentNullException(nameof(execute));
            this.canExecute = canExecute;
 
            requerySuggested = (o, e) => Invalidate();
            CommandManager.RequerySuggested += requerySuggested;
        }
 
        /// <inheritdoc cref="RelayCommand(ExecuteHandler, CanExecuteHandler)"/>
        public RelayCommand(Action execute, Func<bool> canExecute = null)
                : this
                (
                      p => execute(),
                      p => canExecute?.Invoke() ?? true
                )
        { }
 
        private RelayCommand()
            => dispatcher = Application.Current.Dispatcher;
 
        private readonly Dispatcher dispatcher;
 
        /// <summary>Метод, подымающий событие <see cref="CanExecuteChanged"/>.</summary>
        public void RaiseCanExecuteChanged()
        {
            if (dispatcher.CheckAccess())
                Invalidate();
            else
                dispatcher.BeginInvoke((Action)Invalidate);
        }
        private void Invalidate()
            => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
 
        /// <summary>Вызов метода, возвращающего состояние команды.</summary>
        /// <param name="parameter">Параметр команды.</param>
        /// <returns><see langword="true"/> - если выполнение команды разрешено.</returns>
        public bool CanExecute(object parameter) => canExecute?.Invoke(parameter) ?? true;
 
        /// <summary>Вызов выполняющего метода команды.</summary>
        /// <param name="parameter">Параметр команды.</param>
        public void Execute(object parameter) => execute?.Invoke(parameter);
    }
    #endregion
}
Так же нет маршалинга события INotifyCollectionChanged.CollectionChanged в ItemsControl. Приходится либо в VM маршализировать изменения коллекций в UI поток, либо синхронизировать эти изменения с привязками методом BindingOperations.EnableCollectionSynchronization(...).

Ладно, встроили всё это в VM для WPF. НО!!! Для UWP её использовать не получится, хотя во всей остальной логике никаких изменений не понадобится.
В UWP нет маршалинга INotifyPropertyChanged.PropertyChanged. Зато есть для INotifyCollectionChanged.CollectionChanged и ICommand.CanExecuteChanged. Так же не получится сделать общий маршалинг и подписку команд, так как в UWP нет CommandManager и другой Dispatcher.

В результате мы получаем, что для WPF и UWP нужны полностью одинаковые VM по основной своей функциональности - отражение Модели в своих свойствах.
Но разные на уровне базовой реализации в которые встроены маршалинг и подписка команд.

Чего хочется.
Так как отличия относятся к деталям реализации типа Представления, то разрешать это на уровне Представления, а не на уровне ViewModel.

Как решаю в текущее время.
Через рефлексию при первом обращении пытаюсь определить тип Представления и "подсовываю" через фабрику нужные базовые типы. Получается очень муторно, путано, не уверен, что будет работать всегда и точно будет работать только для тех типов, что я "железно вшил" в базовую реализацию. Какую-то гибкую настройку по новому типу GUI добавить не получится. Придётся менять исходный библиотечный код.
0
cpp_developer
Эксперт
20123 / 5690 / 1417
Регистрация: 09.04.2010
Сообщений: 22,546
Блог
07.01.2023, 14:19
Ответы с готовыми решениями:

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

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

Mvvm ViewModel в ViewModel
Есть одно окошко там много вкладок, в каждой вкладке есть подвкладки и там много разных таблиц и кнопок. ViewModel будет просто огромной,...

16
HF
 Аватар для HF
1303 / 882 / 199
Регистрация: 09.09.2011
Сообщений: 2,590
Записей в блоге: 2
07.01.2023, 19:44
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Хоть вопрос и проистекает напрямую из реализации MVVM и, соответственно, тесно связан с WPF, но данный аспект, как мне кажется, больше относится к Шарпу в целом.
Видимо опять внесу критику, но моё мнение что вы всё равно сильно завязаны именно на WPF и цель - именно эта проблема. Я даже не понял причём тут два разных события - PropertyChanged и CollectionChanged? Это ведь вроде бы и объекты разные? А если так, то код в итоге на разных платформах - совершенно разный.
Да, видимо одним из вариантов вы предполагаете - правильная абстракция. Но это тогда и должна быть абстрактная тема про абстрактные классы с общими методами делающими потенциально что-то одно и тоже.
Вообщем, лично я пока не вижу причин или чистых решений делать что-то универсальное.

А насчёт абстракций. Если вы действительно можете свои методы каждой платформы сгруппировать в какой-то общий интерфейс. То это уже и будет решение - типа сервис INotifyChanger, а в каждом проекте свои реализации типа IWpfPropertyChanger, IUwpPropertyChanger. И уж пусть они там делают Invalidate или не делают.
Но это явно уже тянет на создание своего мультиплатформенного минифреймворка или библиотеки.
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16117 / 11238 / 2887
Регистрация: 21.04.2018
Сообщений: 33,038
Записей в блоге: 2
08.01.2023, 15:30  [ТС]
HF, попробую на примере .

Есть некая Модель с асинхронным событием:
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
using System;
using System.Diagnostics;
using System.Threading.Tasks;
 
namespace StandardModel
{
    public class AsyncModel
    {
        private int number;
 
        public AsyncModel()
        {
            Task.Run(RefreshAsync);
        }
 
        public event EventHandler<NumberChangedArgs> NumberChanged;
 
        private async void RefreshAsync()
        {
            while (true)
            {
                await Task.Delay(1_000);
 
                number++;
                NumberChangedArgs args = new NumberChangedArgs(number);
 
 
                if (args != null)
                {
                    try
                    {
                        NumberChanged?.Invoke(this, args);
                    }
                    catch (Exception exp)
                    {
                        Debug.WriteLine(exp.Message);
                    }
                }
            }
        }
    }
}
Есть VM:
C#
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    public class ViewModel : ViewModelBase
    {
        public readonly AsyncModel model = new AsyncModel();
        private int _number;
 
        public int Number { get => _number; set => Set(ref _number, value); }
 
        public RelayCommand SomeCommand { get; }
        public ViewModel()
        {
            model.NumberChanged += OnValuesChanged;
            SomeCommand = new RelayCommand(() => { }, () => (Number & 1) == 0);
        }
 
        private void OnValuesChanged(object? sender, NumberChangedArgs e)
        {
            Number = e.NewNumber;
            SomeCommand.RaiseCanExecuteChanged();
        }
    }
Эту VM использует WPF и UWP приложение с одинаковой компоновкой:
XML
10
11
12
13
14
15
16
17
18
19
20
21
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Viewbox>
            <TextBlock Text="{Binding Number}" MinWidth="20" Margin="10" TextAlignment="Center"/>
        </Viewbox>
        <Viewbox Grid.Row="1">
            <Button Content="Кнопка" Command="{Binding SomeCommand}" MinWidth="20" Margin="10"/>
        </Viewbox>
    </Grid>

Но, из-за разных особенностей WPF и UWP, реализация классов ViewModelBase и RelayCommand разная на этих платформах.

Поэтому приходится код VM дублировать "один в один" в разных сборках.

Добавлено через 7 минут
Цитата Сообщение от HF Посмотреть сообщение
То это уже и будет решение - типа сервис INotifyChanger
Ну, я что-то подобное и сделал.
Пока в виде фабрики.
Но как-то всё это громоздко получается.
0
HF
 Аватар для HF
1303 / 882 / 199
Регистрация: 09.09.2011
Сообщений: 2,590
Записей в блоге: 2
08.01.2023, 16:18
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Ну, я что-то подобное и сделал.
Пока в виде фабрики.
Но как-то всё это громоздко получается.
Без фабрики никак? Обычный резолв через DI невозможно применить?
Это и был начальный посыл - проекты каждой платформы регистрируют свои реализации (сервисы) для этих общих интерфейсов. Понимаю что это громкие слова, идеальное ООП, но именно к этому надо стремиться.
(пример потом гляну)

Добавлено через 14 минут
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Но, из-за разных особенностей WPF и UWP, реализация классов ViewModelBase и RelayCommand разная на этих платформах.
Вот тут, я сразу уточню - я хоть WPF и знаю средненько (просто не сильно часто практикуюсь на ней), но мы всё ж пытаемся найти универсальные интерфейсы для всех этих платформ. Поэтому вам должно быть лучше видно что именно у них отличается - и где в этом можно внедриться чтобы заменить реализацию.

Ну например, вы говорите что RelayCommand разная. Тут я немного засомневался, или в своём знании или в вашем примере. Команды же вроде бы одинаковые в WPF, UWP, Avalonia и т.п. Или имелось ввиду что разные пространства имён?
И если сами команды не совместимы, то.. само действие, внутри команды, можно же сделать "разным"?
Но если вы копируете 1:1 то значит действия одинаковые. Значем нужно копировать?
Вот это я пока не понял. Наверное пока сам "не потрогаю не пойму".

Цитата Сообщение от Элд Хасп Посмотреть сообщение
Поэтому приходится код VM дублировать "один в один" в разных сборках.
Никогда не пользовался, но помоему это основная причина, когда применяют кодогенерацию.
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16117 / 11238 / 2887
Регистрация: 21.04.2018
Сообщений: 33,038
Записей в блоге: 2
08.01.2023, 17:42  [ТС]
Цитата Сообщение от HF Посмотреть сообщение
Без фабрики никак?
Это временное решение. "Прощупывание" различий между платформами.
Сделано всё через рефлексию (для WPF .Net и UWP) и нужные фабрики определяются автоматически.
Но вот как дальше развивать.... Получается нужно будет определять с десяток известных платформ. А если появится ещё какая-то? В рефлексии всё не учтёшь.
Отсюда и появились мысли о том к какому конечному результату следует прийти.

Цитата Сообщение от HF Посмотреть сообщение
Обычный резолв через DI невозможно применить?
Возможно. Вот только насколько оправдано. Хочется чего-то прозрачного автоматического или почти автоматического.
Реализовывал ещё вариант когда маршалинг встраивал в свои кастомные привязки.
Работает неплохо, довольно логично.
Но представил как это на практике в применении....
Нужно вместо дефолтных привязок во всём коде использовать мои. Точно неудобно.

Я до сих пор никак понять не могу, почему в "родных" библах всё это не реализовано?
На их уровне это добавить, вообще, как "два пальца...". Ещё и сделано как-то через ж.... в одной платформе для одной части есть маршалинг, на другой для другой. Какие-то "танцы с бубном" на пустом месте. Может чего-то недопонимаю? Такие сомнения тоже есть.

Добавлено через 5 минут
Цитата Сообщение от HF Посмотреть сообщение
Ну например, вы говорите что RelayCommand разная.
Не только.
Тот же INPC нужно по разному подымать в WPF и UWP в многопоточном приложении.

Из ViewModelBase для WPF - всё привычно:
C#
11
12
13
14
        protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName ?? string.Empty));
        }
А в ViewModelBase для UWP уже нужен маршалинг:
C#
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
        public CoreDispatcher Dispatcher { get; } = CoreApplication.MainView.CoreWindow.Dispatcher;
 
        protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
        {
            if (Dispatcher.HasThreadAccess)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName ?? string.Empty));
            }
            else
            {
                Dispatcher
                    .RunAsync
                    (
                        CoreDispatcherPriority.Normal,
                        () => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName ?? string.Empty))
                    )
                    .AsTask().Wait();
            }
        }
Добавлено через 3 минуты
Для RelayCommand наоборот. Для WPF нужен маршалинг:
C#
29
30
31
32
33
34
35
        public void RaiseCanExecuteChanged()
        {
            if (dispatcher.CheckAccess())
                Invalidate();
            else
                dispatcher.BeginInvoke(Invalidate);
        }
А для UWP не нужен:
C#
24
25
26
27
        public void RaiseCanExecuteChanged()
        {
            CanExecuteChanged?.Invoke(this, EventArgs.Empty);
        }
Добавлено через 9 минут
HF, есть ещё вариант с определением потока при подключении прослушки. И если у него есть контекст, то подымать его в этом же потоке.
Основано на этом коде: RelayCommandSyncContext.

Добавлено через 56 минут
Цитата Сообщение от HF Посмотреть сообщение
Никогда не пользовался, ... ,когда применяют кодогенерацию
Ну, если использовали Формы или WPF, то пользовались.
Просто не знали об этом.
По XAML кодогенератором создаётся файл *.g.i.cs, в котором содержится код частичного класса для Code Front.
Именно из-за наличия этого файла вы и можете обращаться в Code Behind к элементам по именам.

Для Форма файл другой, но по сути аналогично.

Когенерация здесь не поможет.
Она может, как например в ToolKit, автоматически для приватного поля создать публичное свойство для доступа у нему.
0
HF
 Аватар для HF
1303 / 882 / 199
Регистрация: 09.09.2011
Сообщений: 2,590
Записей в блоге: 2
08.01.2023, 17:45
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Когенерация здесь не поможет.
Она может, как например в ToolKit, автоматически для приватного поля создать публичное свойство для доступа у нему.
Да разве ж? Целыми классами можно генерировать.
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16117 / 11238 / 2887
Регистрация: 21.04.2018
Сообщений: 33,038
Записей в блоге: 2
08.01.2023, 18:05  [ТС]
Цитата Сообщение от HF Посмотреть сообщение
Целыми классами можно генерировать.
Да, не только может, но только так и делает.
В тех же *.g.i.cs - это же коды классов.
Но в данном случае мне же не писать под каждую VM свой кодогенератор.
И как их потом будут факту использовать?
Они рассчитаны, по больше части, на генерацию часто повторяющегося кода. Что-то вроде "навороченный сниппетов", которые создают код в отдельном файле и могут его самостоятельно очень гибко настраивать.

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

Для команд это более менее понятно. Они создаются экземплярами, поэтому фабрика хорошо подходит. Нужную. реализацию можно фиксировать при старте приложения (через какую-то службу). Но здесь тоже есть свои нюансы для Времени Разработки, так как в это время регистрация не будет исполняться. Или опять придётся придумывать какие-то неочевидные хитрости.

А вот с VM, вообще, засада. Здесь же не экземпляр создаётся, а нужно создавать производный класс.
И как здесь изменять базовый без пересборки - ума не приложу.
Придётся наверное PropertyChanged делать прокси свойством. А событие подымать в каком-то встроенном внутреннем (приватном) экземпляре, который и будет меняться через фабрику или службу.
0
Эксперт .NET
 Аватар для kolorotur
17823 / 12973 / 3382
Регистрация: 17.09.2011
Сообщений: 21,261
10.01.2023, 17:02
Цитата Сообщение от Элд Хасп Посмотреть сообщение
В WPF событие INotifyPropertyChanged.PropertyChanged можно подымать в любом потоке, так как его маршалинг в UI поток встроен в WPF привязки.
Цитата Сообщение от Элд Хасп Посмотреть сообщение
А вот для события ICommand.CanExecuteChanged в кнопках и пунктах меню такого маршилинга нет.
Что и куда должно маршаллиться и как это делать — это, по-хорошему, головная боль тех, кто реагирует на событие, а не тех, кто его поднимает.

Цитата Сообщение от Элд Хасп Посмотреть сообщение
Мне же нужен, какой-то простой, понятный всем способ смены базовых классов без затрагивания конечного кода создаваемого разработчиком.
Условное комплирование.
Разработка под разные UI-фреймворки здесь ничем не отличается от разработки под разные платформы: у каждого свои заморочки и полностью универсальным код может быть только тогда, когда все целевые платформы ведут себя абсолютно одинаково (зачем они тогда нужны разные?).

Цитата Сообщение от Элд Хасп Посмотреть сообщение
Поэтому приходится код VM дублировать "один в один" в разных сборках.
Не надо код дублировать, достаточно чтобы два проекта работали с одними и теми же исходниками.
Создаете два файла проекта: ViewModel.WPF.csproj и ViewModel.UWP.csproj с соответствующими зависимостями, помещаете их в одну папку, чтобы оба видели одну и ту же структуру папок и файлов с исходниками.
Добавляете оба проекта в решение, в ViewModel.WPF устанавливаете флаг WPF, в ViewModel.UWP — UWP, потом в исходниках стратегично вставляете #if/#else.
На вашем примере:
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
public class BaseVM : INotifyPropertyChanged
{
#if UWP
    public CoreDispatcher Dispatcher { get; } = CoreApplication.MainView.CoreWindow.Dispatcher;
#endif
 
    protected void OnPropertyChanged(PropertyChangedEventArgs e)
    {
#if WPF
        PropertyChanged?.Invoke(this, e);
#elif UWP
        if (Dispatcher.HasThreadAccess)
        {
            PropertyChanged?.Invoke(this, e);
        }
        else
        {
            Dispatcher.RunAsync
            (
                CoreDispatcherPriority.Normal,
                () => PropertyChanged?.Invoke(this, e)
            ).AsTask().Wait();
        }
#else
        throw new NotSupportedException("Neither UWP, nor WPF - must be WTF");
#endif
    }
}
Потом все это дело компилируете и на выходе получаете две dll и/или два NuGet-пакета — по одному на каждую платформу.
Код в результате один, с условными изменениями под индивидуальные заморочки фреймворка/платформы.
1
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16117 / 11238 / 2887
Регистрация: 21.04.2018
Сообщений: 33,038
Записей в блоге: 2
10.01.2023, 19:50  [ТС]
Цитата Сообщение от kolorotur Посмотреть сообщение
Что и куда должно маршаллиться и как это делать — это, по-хорошему, головная боль тех, кто реагирует на событие, а не тех, кто его поднимает.
Мне такая реализация кажется наиболее правильной - полностью с вами согласен.
Но как заставить UWP самостоятельно маршалировать PropertyChanged в UI поток?
Это без проблем можно встроить в привязки, но для этого -же надо менять их исходный код.
Можно создать свой класс привязок, но с практическим их применением тоже будет не мало головняков.

Я поэтому и интересуюсь как концептуально и практически лучше сделать.

Цитата Сообщение от kolorotur Посмотреть сообщение
На вашем примере:
Не то - это условная компиляция.
И кроме директив предпроцессора нужно ещё и разные ссылки на сборки подключать.

Вариант с поздним связыванием (на уровня слушателя - View) концептуально - идеален.
Но я не понимаю как его на практике "красиво" реализовать.

Сейчас получилось сделать только через рефлексию.
Определяю какие сборки загружены приложением и вытягиваю или System.Windows.Threading.Dispatcher или Windows.UI.Core.CoreDispatcher.
Но что-то мне кажется это так "грязно"....
0
Эксперт .NET
 Аватар для kolorotur
17823 / 12973 / 3382
Регистрация: 17.09.2011
Сообщений: 21,261
10.01.2023, 20:07
Цитата Сообщение от Элд Хасп Посмотреть сообщение
И кроме директив предпроцессора нужно ещё и разные ссылки на сборки подключать.
Именно для этих целей там два проекта — у каждого свои зависимости.
По этой же причине все кроссплатформенные проекты делают отдельные сборки под каждую платформу/фреймворк — тот же CommunityToolkit.
Ну или копировать код, но так в здравом уме никто не делает.

Цитата Сообщение от Элд Хасп Посмотреть сообщение
Сейчас получилось сделать только через рефлексию.
А пользователи за это платят
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16117 / 11238 / 2887
Регистрация: 21.04.2018
Сообщений: 33,038
Записей в блоге: 2
10.01.2023, 21:22  [ТС]
Цитата Сообщение от kolorotur Посмотреть сообщение
Именно для этих целей там два проекта — у каждого свои зависимости.
Вот этого и хотелось избежать...

Цитата Сообщение от kolorotur Посмотреть сообщение
А пользователи за это платят
В простом варианте - одноразово при первом обращении к платфомозависимому члену.

"Беда" рефлексии - это то что заранее нужно прописывать все возможные платформы.
То есть нужно заранее для всех платформ прописать реализацию платформозависимых типов и членов.
0
HF
 Аватар для HF
1303 / 882 / 199
Регистрация: 09.09.2011
Сообщений: 2,590
Записей в блоге: 2
10.01.2023, 21:44
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Мне такая реализация кажется наиболее правильной - полностью с вами согласен.
Но как заставить UWP самостоятельно маршалировать PropertyChanged в UI поток?
Примерно то что я и сказал - нужно искать код, который работает везде, а в платформенных только его использование по интерфейсу.
Но вы снова начинаете возвращаться к коду зависимому от платформ. А значит - или вы не нашли этот "базовый универсальный код" и вы всё равно гнёте свою линию.
Или же в вашей реализации это невозможно и нужно и можно "копипастить".

kolorotur предложил отличный вариант - с управляющими командами компиляции. Про них я и забыл. Действительно, хоть это грязно, но если такого кода не много, то это намного универсальнее, чем копипастить кучу классов.

Цитата Сообщение от Элд Хасп Посмотреть сообщение
"Беда" рефлексии - это то что заранее нужно прописывать все возможные платформы.
То есть нужно заранее для всех платформ прописать реализацию платформозависимых типов и членов.
Всегда удивлялся вашему подходу. Когда я слышу про рефлексию, я от неё бегу. Не, я знаю как её использовать и использую где надо. Но так чтобы вот брать и на ней строить приложение и даже не сомневаться в верности идеи. Ну такое... смело и странно.
Даже вот так. Вопрос - не уже ли код через рефлексию будет универсальным и поддерживаемым? Мне кажется вы сделаете что-то рабочее, но на один раз. Больше вы его видеть не захотите вообще.
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16117 / 11238 / 2887
Регистрация: 21.04.2018
Сообщений: 33,038
Записей в блоге: 2
10.01.2023, 23:21  [ТС]
Цитата Сообщение от HF Посмотреть сообщение
Но вы снова начинаете возвращаться к коду зависимому от платформ.
Я не возвращаюсь.
Я пишу как у меня сделано. И тему эту завёл потому, что мне это не нравится.

Цитата Сообщение от HF Посмотреть сообщение
Всегда удивлялся вашему подходу. Когда я слышу про рефлексию, я от неё бегу.
Это особенность WPF, там если капнуть, всё на рефлексии построено.
Те же привязки "под капотом" полностью построены на рефлексии. Иначе как можно было бы по имени свойства получать и задавать его значение?

Что-то супер-пупер универсальное, абстрактное приходится строить на рефлексии.
Недавно делал что-то типа автотаблицы свойств для любого типа. Как тут без рефлексии.

В данном случае я пока ищу лучшее решение. Из тех что уже реализовал
1) Это вариант на репозиторий которого давал ссылку выше https://github.com/EldHasp/CrossPlatformViewModel
В нём VM и её базовые классы - отдельные ветки для каждой платформы;
2) Вариант на рефлексии. Пока реализован только для команд. В нём есть универсальная фабрика команд в которой при старте регистрируется нужный базовый тип.

Что мне кажется оптимальным. Это перенос всего зависимого от платформы View кода, собственно в View. На моём уровне знаний это можно сделать двумя способами:
3) Применением кастомных привязок в которых встроена адаптация объектов источников под платформу View;
4) Инжекция нужных типов в фабрику, которая будет создавать их экземпляры. Это то что вы предлагали.

Первый вариант сильно изменит XAML компоновки.
Второй вызывает конфликты в Дизайнере XAML в Режиме Разработки, которые пока я не смог придумать как обойти.
К сожалению в WPF многие вещи в рантайм и в дизайнере работаю по разному.

Вариант от kolorotur, если я правильно, это немного усовершенствованный вариант 1.
Есть ещё идейно близкий вариант - предложенный вами вариант с кодогенератором, который вставляет код с нужным базовым классом.
1
HF
 Аватар для HF
1303 / 882 / 199
Регистрация: 09.09.2011
Сообщений: 2,590
Записей в блоге: 2
11.01.2023, 00:06
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Второй вызывает конфликты в Дизайнере XAML в Режиме Разработки, которые пока я не смог придумать как обойти.
К сожалению в WPF многие вещи в рантайм и в дизайнере работаю по разному.
Кстати да, я в тот раз обратил внимание, но забыл спросить.
Реалвью в дизайнере это конечно клёво и удобно, но если только это останавливает вас реализовать более менее приемлимый вариант - то подумайте ещё раз. Мы жили без реалвью и уж можно наверное ради такого ещё поработать без него?

(я правильно понял о чём речь? - когда он сразу отображает дизайн с наполнением, хотя это было реализовано через код а не через дизайн. Фишки позволяющие "подсовывать реалистичную симуляцию" (уж не знаю как это назвать)).
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16117 / 11238 / 2887
Регистрация: 21.04.2018
Сообщений: 33,038
Записей в блоге: 2
11.01.2023, 10:02  [ТС]
Цитата Сообщение от HF Посмотреть сообщение
Фишки позволяющие "подсовывать реалистичную симуляцию"
Да, всё верно.
В View можно определять это Режим разработки или нет. По крайней мере для WPF и UWP точно можно.
И можно этот параметр передать в VM или вызывать VM Времени Разработки.
VM, соответственно, подсовывает демо данные для этого режима.
Можно прокидывать параметр и дальше в сервисы и Модель чтобы те тоже возвращали демо данные.
И получается очень удобная разработка и одновременная интерактивная отладка компоновки.
Это всё нормально работает и надо только уметь этим пользоваться.

Засада кроется немного в другом.
В данном, случае для DI нужна регистрация. Делать её нужно в CB App. В конструкторе или событии Startup.
А CB в Режиме разработки не исполняется. Получается что нет и регистрации. И теперь если я в VM буду создавать (например) команду тип которой должен получить от DI, то поймаю исключение. И Дизайнер XAML обрушится.
В рантайм оно-то всё будет работать. Но создавать "всплепую" XAML компоновку... так себе удовольствие.

Цитата Сообщение от HF Посмотреть сообщение
Реалвью в дизайнере это конечно клёво и удобно, но если только это останавливает вас реализовать более менее приемлимый вариант - то подумайте ещё раз.
Сделать так чтобы работало - это не проблема. И в этом меня ничего не останавливает.
Да и есть достаточно платформ для MVVM чтобы конкурировать с ними.
Данный вопрос это скорее для собственного развития, изучения "труднодоступных" возможностей WPF/UWP и дань перфекционизму.
0
1338 / 918 / 264
Регистрация: 08.08.2014
Сообщений: 2,759
11.01.2023, 12:17
Цитата Сообщение от Элд Хасп Посмотреть сообщение
Мне же нужен, какой-то простой, понятный всем способ смены базовых классов без затрагивания конечного кода создаваемого разработчиком.
Через кодогенерацию наследных VM?
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//Это реализует программист, это именно VM со всей её логикой.
public class SomeViewModel
{
    public virtual ICollection<string> Items { get; } = default!;
}
 
//Этот класс полностью кодогенерируется либо заранее, на этапе сборки, 
//либо прямо в рантайме при старте приложения (либо при старте с кэшированием на диск). 
//В т.ч. конструкторы. В т.ч. через это можно сделать свою реализацию INPC и подстановки 
//реализации команд конкретной платформы в свойства ICommand.
//
//На самом деле даже виртуалить не обязательно, можно просто в конструкторе 
//сгенерировать создание/присвоение коллекции нужного типа (виртуальность использовал для 
//переопределения поведения сеттеров при реализации INPC через этот подход).
//
//Далее, при старте приложения:
//  в зависимости от платформы генерируется/загружается нужная сборка с прокси-VM
//  прокси-VM регистрируются в DI:
//      sc.AddTransient(typeof(SomeViewModel), typeof(SomeViewModel_WpfProxy));
public class SomeViewModel_WpfProxy : SomeViewModel
{
    public override ICollection<string> Items { get; } = new BindingList<string>();
}
Реализация внутренностей не особо сложна, но придётся разобраться либо с эмитом, либо с рослином.

Реализация клиентского кода - соблюдение некоторых соглашений по используемым типам и начальной инициализации.

Но я это только на уровне экспериментов реализовывал для INPC (вместо Fody) и команд. В целом, работает, но на реальных проектах опыта не было.

Цитата Сообщение от Элд Хасп Посмотреть сообщение
И можно этот параметр передать в VM или вызывать VM Времени Разработки
Один из вариантов разделения VM времени разработки и времени исполнения - через debug-конструктор без параметров:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public sealed class SomeViewModel
{
    public List<string> Items { get; }
 
#if DEBUG
    public SomeViewModel()
    {
        Items = new() { "Item 1", "Item 2" };
    }
#else
    public SomeViewModel(ItemsService itemsService)
    {
        Items = itemsService.GetItems();
    }
#endif
}
0
Модератор
Эксперт .NET
 Аватар для Элд Хасп
16117 / 11238 / 2887
Регистрация: 21.04.2018
Сообщений: 33,038
Записей в блоге: 2
11.01.2023, 12:30  [ТС]
Цитата Сообщение от kotelok Посмотреть сообщение
Один из вариантов разделения VM времени разработки и времени исполнения - через debug-конструктор без параметров
Да, такое тоже использую.

Цитата Сообщение от kotelok Посмотреть сообщение
Через кодогенерацию наследных VM?
Это что-то аналогичное виртуальным навигационным свойствам с отложенной загрузкой в EF.
Подойдёт для команд, так как это свойства. Но для смены базового класса VM, да, ещё в рантайм ... вряд ли, я думаю.
0
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
raxper
Эксперт
30234 / 6612 / 1498
Регистрация: 28.12.2010
Сообщений: 21,154
Блог
11.01.2023, 12:30
Помогаю со студенческими работами здесь

Платформо-независимая связка QDataStream >> QBitArray >> базовый тип
Стоит задача распарсить QDataStream таким образом: stream &gt;&gt; bit; if( bit == 0 ) stream &gt;&gt; float; else stream &gt;&gt;...

Независимая форма
Доброго! В процессе работы приложения необходимо создавать независимые формы (что бы у них был свой значек на панели задач, что бы они не...

Независимая панелька
Нашел уже штук 5 программ, которые дают свой индикатор в виде полупрозрачной панельки поверх ВСЕХ других окон/программ. Независимая от...

Независимая страница в wordperess
Коллеги! Как создать в вордпрессе независимую страницу? То есть страницу, ссылки на которой не будет с главной страницы или с иной...

Независимая копия террейна
Здрасте) Есть две сцены. с первой сцены копирую террейн и вставляю его во вторую сцену. Работаю с террейном во ВТОРОЙ сцене,...


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

Или воспользуйтесь поиском по форуму:
17
Ответ Создать тему
Новые блоги и статьи
PhpStorm 2025.3: WSL Terminal всегда стартует в ~
and_y87 14.12.2025
PhpStorm 2025. 3: WSL Terminal всегда стартует в ~ (home), игнорируя директорию проекта Симптом: После обновления до PhpStorm 2025. 3 встроенный терминал WSL открывается в домашней директории. . .
Как объединить две одинаковые БД Access с разными данными
VikBal 11.12.2025
Помогите пожалуйста !! Как объединить 2 одинаковые БД Access с разными данными.
Новый ноутбук
volvo 07.12.2025
Всем привет. По скидке в "черную пятницу" взял себе новый ноутбук Lenovo ThinkBook 16 G7 на Амазоне: Ryzen 5 7533HS 64 Gb DDR5 1Tb NVMe 16" Full HD Display Win11 Pro
Музыка, написанная Искусственным Интеллектом
volvo 04.12.2025
Всем привет. Некоторое время назад меня заинтересовало, что уже умеет ИИ в плане написания музыки для песен, и, собственно, исполнения этих самых песен. Стихов у нас много, уже вышли 4 книги, еще 3. . .
От async/await к виртуальным потокам в Python
IndentationError 23.11.2025
Армин Ронахер поставил под сомнение async/ await. Создатель Flask заявляет: цветные функции - провал, виртуальные потоки - решение. Не threading-динозавры, а новое поколение лёгких потоков. Откат?. . .
Поиск "дружественных имён" СОМ портов
Argus19 22.11.2025
Поиск "дружественных имён" СОМ портов На странице: https:/ / norseev. ru/ 2018/ 01/ 04/ comportlist_windows/ нашёл схожую тему. Там приведён код на С++, который показывает только имена СОМ портов, типа,. . .
Сколько Государство потратило денег на меня, обеспечивая инсулином.
Programma_Boinc 20.11.2025
Сколько Государство потратило денег на меня, обеспечивая инсулином. Вот решила сделать интересный приблизительный подсчет, сколько государство потратило на меня денег на покупку инсулинов. . . .
Ломающие изменения в C#.NStar Alpha
Etyuhibosecyu 20.11.2025
Уже можно не только тестировать, но и пользоваться C#. NStar - писать оконные приложения, содержащие надписи, кнопки, текстовые поля и даже изображения, например, моя игра "Три в ряд" написана на этом. . .
Мысли в слух
kumehtar 18.11.2025
Кстати, совсем недавно имел разговор на тему медитаций с людьми. И обнаружил, что они вообще не понимают что такое медитация и зачем она нужна. Самые базовые вещи. Для них это - когда просто люди. . .
Создание Single Page Application на фреймах
krapotkin 16.11.2025
Статья исключительно для начинающих. Подходы оригинальностью не блещут. В век Веб все очень привыкли к дизайну Single-Page-Application . Быстренько разберем подход "на фреймах". Мы делаем одну. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru