1

Как можно добиться навигации, так, чтобы по кликам на боковые кнопки (1, 2, 3 слева), происходила смена view справа?

Научился делать смену всего контента в окне, через navigationService при помощи MvvmCross, а вот с частичной не могу разобраться.

введите сюда описание изображения

  • Навигацию сделать легко, а при чем тут MvvmCross? Покажите разметку и код, и расскажите, что именно не получается? У меня есть пример решения, но на базе IoC контейнера Autofac и без MvvmCross. – aepot Apr 04 '21 at 16:31
  • MvvmCross здесь просто для того, что хочется изучить его систему навигации. Я понимаю, что есть разные способы реализации моего вопроса. Также предполагаю, что нет особого смысла прикладывать код, т.к. кроме двух вьюх и вью моделей там больше ничего нет. Я больше хотел получить пример реализации кастомного presenter'а, как это делается в MvvmCross (если все правильно понял) – chesh111re Apr 04 '21 at 17:26
  • Здесь на самом деле есть примеры, но они практически все нарушают MVVM, загоняя список страниц Page в ViewModel и привязывая к ним левый листбокс, который представляет из себя меню. Поэтому пока я ссылок не дам. А контентпрезентер здесь особо не при чем, для навигации между Page используется <Frame>. Сейчас пытаюсь прикрутить это всё без нарушения MVVM и кучи кода, а нативными для WPF+MVVM методами. Если получится, скину пример. – aepot Apr 04 '21 at 17:54

2 Answers2

6

Здесь много примеров реализации многостраничного интерфейса, в основном на базе Frame+Page. Но самый простой, и с виду самый адекватный способ использования этой конструкции нарушает MVVM, потому что коллекцию List<Page> придется хранить в ViewModel, то есть View внутри ViewModel, а VM не должна отвечать за View в MVVM. Это так же пораждает путаницу. Для активной страницы придется выдергивать DataContext, чтобы получить текущую VM страницы в главной VM и еще много других приключений. Базовый способ использования Frame+Page есть в этом ответе.

Но я предлагаю пойти немного другим путем. В качестве View я предлагаю использовать не какой-либо Control, а просто xaml разметку без кодбиханда - DataTemplate, помещаемый в ResourceDictionary.

Структура проекта

введите сюда описание изображения

Так как я не использую MVVM фреймворки, у меня есть класс, реализующий INotifyPropertyChanged, но вы его можете заменить на ViewModelBase, или что там у вас.

NotifyPropertyChanged.cs

public class NotifyPropertyChanged : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Создаем 2 View и 2 ViewModel для страничек, они будут одинаковые, поэтому я покажу только первую.

View1.xaml

<ResourceDictionary 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"
                    mc:Ignorable="d"
                    xmlns:vm="clr-namespace:WpfIoC.ViewModels;assembly=WpfIoC">
    <DataTemplate DataType="{x:Type vm:Page1ViewModel}">
        <Grid d:DataContext="{d:DesignInstance vm:Page1ViewModel}">
            <TextBlock Text="{Binding Text}"/>
        </Grid>
    </DataTemplate>
</ResourceDictionary>

Page1ViewModel.cs

public class Page1ViewModel : NotifyPropertyChanged, IPageViewModel
{
    public string Title => "Page1";
    public string Text => "Page one";
}

Ах, да, интерфейс

IPageViewModel.cs

public interface IPageViewModel
{
    string Title { get; }
}

Интерфейс этот нужен, чтобы можно было засунуть все вьюмодели в один список, ну и чтобы иметь доступ к заголовку странички.

Далее, главное окно

MainWindow.xaml

<Window x:Class="WpfIoC.MainWindow"
        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:vm="clr-namespace:WpfIoC.ViewModels"
        mc:Ignorable="d"
        Height="400" Width="600"
        d:DataContext="{d:DesignInstance vm:MainViewModel, IsDesignTimeCreatable=True}"
        Title="{Binding Title}">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <ListBox BorderThickness="0" Background="AliceBlue" ItemsSource="{Binding PageViewModels}" SelectedItem="{Binding SelectedPageViewModel}" SelectedIndex="0" MinWidth="40">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Title}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <ContentPresenter Grid.Column="1" Content="{Binding SelectedPageViewModel}"/>
    </Grid>
</Window>

В код-бихайнде ничего нет

MainViewWindow.xaml.cs

public sealed partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
}

И его вьюмодель

MainViewModel.cs

public class MainViewModel : NotifyPropertyChanged
{
    private IPageViewModel _selectedPageViewModel;
    private IList<IPageViewModel> _pageViewModels;
public string Title =&gt; $&quot;MultiPage demo - {SelectedPageViewModel?.Title}&quot;;

public IList&lt;IPageViewModel&gt; PageViewModels
{
    get =&gt; _pageViewModels;
    set
    {
        _pageViewModels = value;
        OnPropertyChanged();
    }
}

public IPageViewModel SelectedPageViewModel 
{
    get =&gt; _selectedPageViewModel; 
    set 
    {
        _selectedPageViewModel = value;
        OnPropertyChanged();
        OnPropertyChanged(nameof(Title));
    } 
}

}

Как видите, ничего сверъестественного, но самое главное - точка компоновки приложения (Composition Root), это место, куда вам надо подключать разные части приложения и собирать их в единое целое. Сами по себе эти части друг о друге мало чего знают, или ничего вовсе не знают. Это позволяет разрабатывать приложение модульно. Слабые зависимости между различнвми компонентами приложения дают больше свободы при доработке отдельных компонент.

Для компоновки я выбрал 2 файла:

App.xaml

<Application x:Class="WpfIoC.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Views/View1.xaml"/>
                <ResourceDictionary Source="Views/View2.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

Здесь как раз и прикручиваются DataTemplate к приложению.

App.xaml.cs

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
    new MainWindow
    {
        DataContext = new MainViewModel
        {
            PageViewModels = new List&lt;IPageViewModel&gt;
            {
                new Page1ViewModel(),
                new Page2ViewModel()
            }
        }
    }.Show();
}

}

А здесь все компоненты собираются в одно целое и запускаются.

Но я предпочитаю код компоновки реализовывать с помощью IoC контейнера (Autofac). Несмотря на то что код сборки именно этого приложения выглядит сложнее, контейнер умеет много всего, и пример здесь собран так, чтобы всё связанное с контейнером было в одном файле, через инициализацию свойств. В обычном же случае я использую конструкторы для внедрения зависимостей, а не свойства, тогда код выглядит проще.

App.xaml.cs

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        ContainerBuilder builder = new ContainerBuilder();
    builder.RegisterType&lt;Page1ViewModel&gt;().As&lt;IPageViewModel&gt;().SingleInstance();
    builder.RegisterType&lt;Page2ViewModel&gt;().As&lt;IPageViewModel&gt;().SingleInstance();
    builder.RegisterType&lt;MainViewModel&gt;().OnActivating(e =&gt; e.Instance.PageViewModels = e.Context.Resolve&lt;IList&lt;IPageViewModel&gt;&gt;()).SingleInstance();

    builder.RegisterType&lt;MainWindow&gt;().OnActivating(e =&gt; e.Instance.DataContext = e.Context.Resolve&lt;MainViewModel&gt;());
    IContainer container = builder.Build();

    using ILifetimeScope scope = container.BeginLifetimeScope();
    scope.Resolve&lt;MainWindow&gt;().Show();
}

}

Готово.

введите сюда описание изображения

Если захотите воткнуть юзерконтрол в качестве View, просто вставьте его в нужный DataTemplate.

aepot
  • 49,560
  • 1
    Спасибо большое за столь подробный ответ. Вы действительно можете убедить использовать стандартные свойства WPF :-) – chesh111re Apr 05 '21 at 21:02
  • @chesh111re ну Page+Frame тоже весьма стандартные, только кривоват у них дизайн для MVVM. Я именно с этой парочки и начал путь к решению. – aepot Apr 05 '21 at 21:06
  • @eapot А я правильно понимаю, что в классе App, нужно регистрировать все имеющиеся ViewModel'и? – chesh111re Apr 06 '21 at 05:35
  • @chesh111re это просто точка сборки словарей ресурсов. Вы можете их собрать хоть в окне, хоть в еще одном словаре в отдельном файле, а потом подключать куда надо. Я скорее предложил технологию и способ ее использования. Вам решать, где именно вы подключите xaml файлы. – aepot Apr 06 '21 at 05:41
  • 1
    @chesh111re Вы тогда вопрос редактируйте, чтоб там не было какого-либо упоминания фреймворка, а то сейчас этот ответ банально не в тему, ибо но хотелось бы разобраться с MvvmCross, а в итоге приняли ответ, где все идет в разрез фреймворка... – EvgeniyZ Apr 06 '21 at 15:25
  • @EvgeniyZ согласен, сейчас поправлю – chesh111re Apr 06 '21 at 15:32
  • @aepot xmlns:vm="clr-namespace:WpfIoC.ViewModels;assembly=WpfIoC" можно про это узнать? ибо в DataTemplate ругается на viewmodel созданную UPD: уже понял UPD2: но все равно ругается на DataType – gw gw Feb 25 '23 at 00:32
  • @gwgw бывает xaml глючит, надо пересобрать приложение, после пересборки, если не помоло, перезапустить студию. А вообще это просто неймспейс и класс. – aepot Feb 25 '23 at 00:41
  • @aepot
    1. Ваше решение подразумевает пердачу данных из Page1ViewModel в Page2ViewModel? Или нужно дополнительные классы создавать? Если несложно опишите в двух словах что нужно сделать... Пример: из Page1ViewModel.Property1 передать в Page2ViewModel.Property2.
    – eusataf Dec 11 '23 at 08:37
  • @eusataf а зачем что-либо передавать, если можно просто через конструктор в VM1 получить экземпляр VM2 и просто иметь к ней прямой доступ. Моё решение не про передачу данных, а про сборку логики приложения. И вообще "передача" здесь некорректно, правильнее "доступ". – aepot Dec 11 '23 at 09:32
2

У меня есть более элегантное решение. Как мне кажется. Действовать мы будем в рамках WPF MVVM Frame + Page. Для примера создаем главное окно и три странички.

Структура XAML главного окна:

    <Grid Grid.Row="1">
        <Grid.ColumnDefinitions>
            <ColumnDefinition MaxWidth="150" MinWidth="150"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
    &lt;!--#region Menu panel--&gt;
    &lt;Grid Grid.Column=&quot;0&quot;&gt;
        &lt;Grid.RowDefinitions&gt;
            &lt;RowDefinition MaxHeight=&quot;40&quot;/&gt;
            &lt;RowDefinition MaxHeight=&quot;530&quot;/&gt;
        &lt;/Grid.RowDefinitions&gt;

        &lt;StackPanel Grid.Row=&quot;1&quot;&gt;
            &lt;RadioButton Command=&quot;{Binding ChangePageCommand}&quot; CommandParameter=&quot;1&quot;/&gt;
            &lt;RadioButton Command=&quot;{Binding ChangePageCommand}&quot; CommandParameter=&quot;2&quot;/&gt;
            &lt;RadioButton Command=&quot;{Binding ChangePageCommand}&quot; CommandParameter=&quot;3&quot;/&gt;
        &lt;/StackPanel&gt;
    &lt;/Grid&gt;
    &lt;!--#endregion--&gt;

    &lt;Frame Grid.Column=&quot;1&quot;
           NavigationUIVisibility=&quot;Hidden&quot;
           Content=&quot;{Binding CurrentPage}&quot;/&gt;
&lt;/Grid&gt;

Здесь мы описываем Grid на подобии твоего рисунка. Заместо RadioButton можно использовать простой Button, но как по мне так более логично. Следом Биндим команду ChangePageCommand с параметром (главное отличным от другого). После чего, во второй столбец грида закидываем элемент Frame (он нужен для отображения страниц) и тут же биндимся к CurrentPage. (ПОЗЖЕ ВСЕ бинды СОЗДАДИМ И ПОДКЛЮЧИМ).

Дальше начинается самый сок. Устанавливаем DependencyInjection. В App.xaml.cs пишем следующее:

namespace app
{
    public partial class App : Application
    {
        private static IServiceProvider _service;
        public static IServiceProvider Services => _service ??= InitializeServices().BuildServiceProvider();
    private static IServiceCollection InitializeServices()
    {
        var services = new ServiceCollection();

        services.AddSingleton&lt;IUserDialog, UserDialogServices&gt;();
        // подключаем окно
        services.AddTransient(s =&gt;
        {
            var model = s.GetService&lt;MainWindowViewModel&gt;();
            var window = new MainWindow { DataContext = model };

            return window;
        });
        // подключаем первую страницу и вьюмодел к ней
        services.AddTransient(s =&gt;
        {
            var model = s.GetService&lt;FirstPage&gt;();
            var page = new FirstPage() { DataContext = model };

            return page;
        });
        services.AddTransient(s =&gt;
        {
            var model = s.GetRequiredService&lt;SecondPage&gt;();
            var page = new SecondPage { DataContext = model };

            return page;
        }); 
        services.AddTransient(s =&gt;
        {
            var model = s.GetRequiredService&lt;ThreethPage&gt;();
            var page = new ThreethPage { DataContext = model };

            return page;
        });

        return services;
    }

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        // ставим точку старта на UserDialog с методом OpenMainWindow
        Services.GetRequiredService&lt;IUserDialog&gt;().OpenMainWindow();
    }
}

}

Что такое IUserDialog с этим OpenMainWindow? Объясняю. Тут мы фиксируем все страницы и главное окно.

Код UserDialogServices:

internal class UserDialogServices : IUserDialog
    {
        private readonly IServiceProvider _services;
        public UserDialogServices(IServiceProvider services)
        {
            _services = services;
        }
    private MainWindow _mainWindow;
    private FirstPage _firstPage;
    private SecondPage _secondPage;
    private ThreethPage _threethPage;

    private string _listViewPageTitle;

    public void OpenMainWindow()
    {
        if (_mainWindow is { } window) //если окно существует
        {
            window.Show();
            return; // возвращаем текущее
        }

        window = _services.GetRequiredService&lt;MainWindow&gt;(); // получаем главное окно из di контейнера
        window.Closed += (_, _) =&gt; _mainWindow = null;

        _mainWindow = window; // присваиваем новое созданное окно.
        window.Show();
    }
    public Page OpenFirstPage()
    {
        if (_firstPageis { } page)
        {
            return page;
        }

        page = _services.GetRequiredService&lt;FirstPage&gt;();

        _firstPage = page;
        return page;
    }
    public Page OpenSecondPage()
    {
        if (_secondPages { } page )
        {
            return page;
        }

        page = _services.GetRequiredService&lt;SecondPage&gt;();
        _secondPages = page;

        return page;
    }
    public Page OpenThreethPage()
    {
        if (_threethPage{ } page )
        {
            return page;
        }

        page = _services.GetRequiredService&lt;ThreethPage&gt;();
        _threethPage = page;

        return page;
    }
}

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

internal class MainWindowViewModel : ViewModelBase
    {
        private readonly IUserDialog _userDialog;
    private Page _currentPage;
    public Page CurrentPage 
    {
        get =&gt; _currentPage;
        set =&gt; Set(ref _currentPage, value);
    }

    public MainWindowViewModel(IUserDialog userDialog) : this()
    {
        _userDialog = userDialog;
    }
    public MainWindowViewModel() { }

    #region Смена страницы

    private LambdaCommand _changePageCommand;
    public ICommand ChangePageCommand =&gt; _changePageCommand ??= new(OnChangePageCommand);
    private void OnChangePageCommand(object p)
    {
        switch ((string)p)
        {
            case &quot;1&quot;:
                CurrentPage = _userDialog.OpenFirstPage();
                break;
            case &quot;2&quot;:
                CurrentPage = _userDialog.OpenSecondPage();
                break;
            case &quot;3&quot;:
                CurrentPage = _userDialog.OpenThreethPage();
                break;
        }
    }

    #endregion
}

К команде OnChangePageCommand мы прибиндились ранее. В принимаемом параметре мы указываем какой параметр какую страницу открывает.

Проверяем и радуемся =)

Пишу подобное первый раз, извиняюсь сразу за все недочеты, я старался... Решение не самое лучшее, но используется в моём проекте. Куда лучше будет изучать MVVMCommunityTool, Catel или Prism. Способов много.

yava43
  • 83
  • У меня есть более элегантное решение. - сплошной хардкод, это не круто. При добавлении нового View надо код переписывать в 3 местах, так себе элегантность. В целом идея неплохая, реализацию только отполировать. – aepot Mar 04 '23 at 08:01