Как можно добиться навигации, так, чтобы по кликам на боковые кнопки (1, 2, 3 слева), происходила смена view справа?
Научился делать смену всего контента в окне, через navigationService при помощи MvvmCross, а вот с частичной не могу разобраться.
Как можно добиться навигации, так, чтобы по кликам на боковые кнопки (1, 2, 3 слева), происходила смена view справа?
Научился делать смену всего контента в окне, через navigationService при помощи MvvmCross, а вот с частичной не могу разобраться.
Здесь много примеров реализации многостраничного интерфейса, в основном на базе 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 => $"MultiPage demo - {SelectedPageViewModel?.Title}";
public IList<IPageViewModel> PageViewModels
{
get => _pageViewModels;
set
{
_pageViewModels = value;
OnPropertyChanged();
}
}
public IPageViewModel SelectedPageViewModel
{
get => _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<IPageViewModel>
{
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<Page1ViewModel>().As<IPageViewModel>().SingleInstance();
builder.RegisterType<Page2ViewModel>().As<IPageViewModel>().SingleInstance();
builder.RegisterType<MainViewModel>().OnActivating(e => e.Instance.PageViewModels = e.Context.Resolve<IList<IPageViewModel>>()).SingleInstance();
builder.RegisterType<MainWindow>().OnActivating(e => e.Instance.DataContext = e.Context.Resolve<MainViewModel>());
IContainer container = builder.Build();
using ILifetimeScope scope = container.BeginLifetimeScope();
scope.Resolve<MainWindow>().Show();
}
}
Готово.
Если захотите воткнуть юзерконтрол в качестве View, просто вставьте его в нужный DataTemplate.
Page+Frame тоже весьма стандартные, только кривоват у них дизайн для MVVM. Я именно с этой парочки и начал путь к решению.
– aepot
Apr 05 '21 at 21:06
но хотелось бы разобраться с MvvmCross, а в итоге приняли ответ, где все идет в разрез фреймворка...
– EvgeniyZ
Apr 06 '21 at 15:25
xmlns:vm="clr-namespace:WpfIoC.ViewModels;assembly=WpfIoC" можно про это узнать? ибо в DataTemplate ругается на viewmodel созданную UPD: уже понял UPD2: но все равно ругается на DataType
– gw gw
Feb 25 '23 at 00:32
Page1ViewModel в Page2ViewModel?
Или нужно дополнительные классы создавать?
Если несложно опишите в двух словах что нужно сделать...
Пример: из Page1ViewModel.Property1 передать в Page2ViewModel.Property2.
– eusataf
Dec 11 '23 at 08:37
У меня есть более элегантное решение. Как мне кажется. Действовать мы будем в рамках WPF MVVM Frame + Page. Для примера создаем главное окно и три странички.
Структура XAML главного окна:
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition MaxWidth="150" MinWidth="150"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<!--#region Menu panel-->
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition MaxHeight="40"/>
<RowDefinition MaxHeight="530"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="1">
<RadioButton Command="{Binding ChangePageCommand}" CommandParameter="1"/>
<RadioButton Command="{Binding ChangePageCommand}" CommandParameter="2"/>
<RadioButton Command="{Binding ChangePageCommand}" CommandParameter="3"/>
</StackPanel>
</Grid>
<!--#endregion-->
<Frame Grid.Column="1"
NavigationUIVisibility="Hidden"
Content="{Binding CurrentPage}"/>
</Grid>
Здесь мы описываем 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<IUserDialog, UserDialogServices>();
// подключаем окно
services.AddTransient(s =>
{
var model = s.GetService<MainWindowViewModel>();
var window = new MainWindow { DataContext = model };
return window;
});
// подключаем первую страницу и вьюмодел к ней
services.AddTransient(s =>
{
var model = s.GetService<FirstPage>();
var page = new FirstPage() { DataContext = model };
return page;
});
services.AddTransient(s =>
{
var model = s.GetRequiredService<SecondPage>();
var page = new SecondPage { DataContext = model };
return page;
});
services.AddTransient(s =>
{
var model = s.GetRequiredService<ThreethPage>();
var page = new ThreethPage { DataContext = model };
return page;
});
return services;
}
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// ставим точку старта на UserDialog с методом OpenMainWindow
Services.GetRequiredService<IUserDialog>().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<MainWindow>(); // получаем главное окно из di контейнера
window.Closed += (_, _) => _mainWindow = null;
_mainWindow = window; // присваиваем новое созданное окно.
window.Show();
}
public Page OpenFirstPage()
{
if (_firstPageis { } page)
{
return page;
}
page = _services.GetRequiredService<FirstPage>();
_firstPage = page;
return page;
}
public Page OpenSecondPage()
{
if (_secondPages { } page )
{
return page;
}
page = _services.GetRequiredService<SecondPage>();
_secondPages = page;
return page;
}
public Page OpenThreethPage()
{
if (_threethPage{ } page )
{
return page;
}
page = _services.GetRequiredService<ThreethPage>();
_threethPage = page;
return page;
}
}
Теперь при каждом открытии страницы мы будем проверять: существует ли она уже, и, если существует, то возвращаем, если нет - создаем новую. Дальше подключаем команды во вьюмоделе главного окна. Выглядит примерно вот так:
internal class MainWindowViewModel : ViewModelBase
{
private readonly IUserDialog _userDialog;
private Page _currentPage;
public Page CurrentPage
{
get => _currentPage;
set => Set(ref _currentPage, value);
}
public MainWindowViewModel(IUserDialog userDialog) : this()
{
_userDialog = userDialog;
}
public MainWindowViewModel() { }
#region Смена страницы
private LambdaCommand _changePageCommand;
public ICommand ChangePageCommand => _changePageCommand ??= new(OnChangePageCommand);
private void OnChangePageCommand(object p)
{
switch ((string)p)
{
case "1":
CurrentPage = _userDialog.OpenFirstPage();
break;
case "2":
CurrentPage = _userDialog.OpenSecondPage();
break;
case "3":
CurrentPage = _userDialog.OpenThreethPage();
break;
}
}
#endregion
}
К команде OnChangePageCommand мы прибиндились ранее. В принимаемом параметре мы указываем какой параметр какую страницу открывает.
Проверяем и радуемся =)
Пишу подобное первый раз, извиняюсь сразу за все недочеты, я старался... Решение не самое лучшее, но используется в моём проекте. Куда лучше будет изучать MVVMCommunityTool, Catel или Prism. Способов много.
Pageв ViewModel и привязывая к ним левый листбокс, который представляет из себя меню. Поэтому пока я ссылок не дам. А контентпрезентер здесь особо не при чем, для навигации междуPageиспользуется<Frame>. Сейчас пытаюсь прикрутить это всё без нарушения MVVM и кучи кода, а нативными для WPF+MVVM методами. Если получится, скину пример. – aepot Apr 04 '21 at 17:54