Я новичок в WPF и MVVM. Я потратил два дня, просматривая кучу вопросов и ответов по этой теме, и мне не удалось заставить это работать. Это неизбежно означает, что я упускаю из виду что-то глупое.
Я пытаюсь использовать DataTemplate
вместе с ContentControl
для переключения между контентом DataGrid
, когда пользователь меняет вкладки в элементе управления Fluent:Ribbon
. Я хотел бы повторно использовать представления, поскольку заполнение содержащихся в них DataGrid
может оказаться дорогостоящим.
У меня есть эти представления/модели просмотра:
MainWindow.xaml
/MainWindowViewModel.cs
— главное окно приложения, состоящее из элементов управления Fluent:RibbonWindow
, Fluent:Ribbon
и Fluent:StatusBar
(некоторые из них удалены в моих фрагментах кода ниже для ясности). Этот класс содержит свойство члена для отслеживания «текущего контента» (CurrentViewModel
) и свойства члена для обработки команд, когда пользователь нажимает кнопки в Fluent:Ribbon
. Другие модели представления создаются в этом классе как частные члены.ProviderView.xaml
/ProviderViewModel.cs
— отображает список «Поставщиков» (для целей данного поста просто абстрактное понятие). Представление содержит UserControl
, который содержит элемент управления DataGrid
. DataGrid
привязан к общедоступному свойству Providers
(списку объектов Provider) внутри экземпляра ProviderViewModel
, который является общедоступным свойством MainWindowViewModel
.Когда я запускаю приложение, что также видно в дизайнере, ContentControl
просто содержит строку ViewModel.ProviderViewModel
, как будто он понятия не имеет, что делать с элементом управления, или я неправильно его создаю.
MainWindow.xaml
<Fluent:RibbonWindow x:Class = "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:ViewModel"
xmlns:local = "clr-namespace:MyProgram"
xmlns:Fluent = "urn:fluent-ribbon"
mc:Ignorable = "d"
Width = "800"
Height = "600"
Name = "MainRibbonWindow"
Icon = "{DynamicResource logo}">
<Fluent:RibbonWindow.Resources>
<DataTemplate x:Key = "g_ProviderViewModel" DataType = "{x:Type vm:ProviderViewModel}">
<local:ProviderView/>
</DataTemplate>
</Fluent:RibbonWindow.Resources>
<Fluent:RibbonWindow.DataContext><vm:MainWindowViewModel/></Fluent:RibbonWindow.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "*" />
<RowDefinition Height = "Auto" />
</Grid.RowDefinitions>
<Fluent:Ribbon VerticalAlignment = "Top"
IsDisplayOptionsButtonVisible = "False"
Name = "MainWindowRibbon"
SelectedTabChanged = "MainWindowRibbon_SelectedTabChanged">
<!--Tabs-->
<Fluent:RibbonTabItem Header = "Providers" Name = "ProvidersTab">
<Fluent:RibbonGroupBox Header = "Options" Width = "120">
<Fluent:Button Header = "Refresh"
Icon = "{DynamicResource refresh}"
Command = "{Binding LoadProvidersCommand}"/>
</Fluent:RibbonGroupBox>
</Fluent:RibbonTabItem>
</Fluent:Ribbon>
<StackPanel HorizontalAlignment = "Stretch" VerticalAlignment = "Stretch" Grid.Row = "1">
<ContentControl Content = "{Binding CurrentViewModel}"/>
</StackPanel>
</Grid>
</Fluent:RibbonWindow>
MainWindowViewModel.cs
namespace MyProgram.ViewModel
{
class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ViewModelBase _CurrentViewModel;
public ViewModelBase CurrentViewModel
{
get => _CurrentViewModel;
set
{
_CurrentViewModel = value;
OnPropertyChanged("CurrentViewModel");
}
}
public ProviderViewModel m_ProviderViewModel = new ProviderViewModel();
private ProviderManifestViewModel m_ProviderManifestViewModel = new ProviderManifestViewModel();
private ICommand _loadProvidersCommand;
public ICommand LoadProvidersCommand
{
get
{
return _loadProvidersCommand ?? (_loadProvidersCommand = new AsyncRelayCommand(Command_LoadProviders, GlobalUiCanExecute));
}
}
private AsyncRelayCommand<Guid> _loadProviderCommand;
public AsyncRelayCommand<Guid> LoadProviderCommand
{
get
{
return _loadProviderCommand ?? (_loadProviderCommand = new AsyncRelayCommand<Guid>(Command_LoadProvider));
}
}
#endregion
public MainWindowViewModel()
{
CurrentViewModel = m_ProviderViewModel;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void ShowProviderViewModel()
{
CurrentViewModel = m_ProviderViewModel;
}
private async Task Command_LoadProviders()
{
g_UiBusy = true;
CurrentViewModel = m_ProviderViewModel;
await m_ProviderViewModel.LoadProviders();
g_UiBusy = false;
}
private async Task<MyProvider?> Command_LoadProvider(Guid Id)
{
if (!GlobalUiCanExecute())
{
return null;
}
g_UiBusy = true;
var provider = await m_ProviderViewModel.LoadProvider(Id);
g_UiBusy = false;
return provider;
}
}
}
ProviderView.xaml
<UserControl x:Class = "MyProgram.ProviderView"
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:MyProgram.ViewModel"
xmlns:Fluent = "urn:fluent-ribbon"
mc:Ignorable = "d"
d:DesignHeight = "450" d:DesignWidth = "800"
Name = "ProviderViewControl">
<UserControl.DataContext><local:MainWindowViewModel/></UserControl.DataContext>
<Grid Name = "ProvidersGrid">
<DataGrid Name = "ProvidersDataGrid"
IsReadOnly = "true"
AutoGenerateColumns = "false"
ItemsSource = "{Binding m_ProviderViewModel.Providers}">
<DataGrid.Columns>
<DataGridTextColumn Header = "ID" Binding = "{Binding Id}"/>
<DataGridTextColumn Header = "Name" Binding = "{Binding Name}" />
<DataGridTextColumn Header = "Source" Binding = "{Binding Source}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>
ProviderViewModel.cs
namespace MyProgram.ViewModel
{
class ProviderViewModel : ViewModelBase
{
private ObservableCollection<MyProvider> _providers;
public ObservableCollection<MyProvider> Providers
{
get => _providers;
set
{
_providers = value;
OnPropertyChanged("Providers");
}
}
public ProviderViewModel()
{
_providers = new ObservableCollection<MyProvider>();
}
public async Task LoadProviders()
{
var providers = await ProviderLoader.GetProviders();
if (providers == null)
{
return;
}
providers.ForEach(f => Providers.Add(f));
}
}
}
Попробуй это. Заменять:
<StackPanel HorizontalAlignment = "Stretch" VerticalAlignment = "Stretch" Grid.Row = "1">
<ContentControl Content = "{Binding CurrentViewModel}"/>
</StackPanel>
С:
<StackPanel HorizontalAlignment = "Stretch" VerticalAlignment = "Stretch" Grid.Row = "1">
<local:ProviderView DataContext = "{Binding CurrentViewModel}"/>
</StackPanel>
Удалить из ProviderView.xaml:
<UserControl.DataContext><local:MainWindowViewModel/></UserControl.DataContext>
Продолжая мой предыдущий комментарий: я хочу сохранить экземпляр ProviderViewModel
«глобальным», потому что его заполнение обходится дорого. По этой причине оно у меня живет в MainWindowViewModel
. Кроме того, чтобы переключать представления на основе выбора вкладки, MainWindowViewModel
должен управлять экземпляром. В результате я не могу установить DataContext
из ProviderView
XAML в MainWindowViewModel
, потому что это создаст дубликат экземпляра этого класса.
Добро пожаловать, я опубликовал свой ответ, чтобы получить дополнительную информацию, я проверю и сообщу вам.
Я хочу знать, правильно ли я вас понимаю, вы хотите MainWindowViewModel.cs
только контролировать ProviderViewModel
, иначе ProviderViewModel
не зависеть от MainWindowViewModel.cs
Я понял... решение заключалось в том, чтобы добавить App.xaml
: <vm:MainWindowViewModel x:Key = "g_MainWindowViewModel" />
. А затем укажите это из представлений MainWindow.xaml
и ProviderView.xaml
в атрибуте управления: DataContext = "{StaticResource g_MainWindowViewModel}"
@user1229658 user1229658 Вам не нужно явно задавать DataContext для ContentControl. Вы уже установили ProviderViewModel
как значение свойства ContentControl.Content
. Достаточно. DataTemplate теперь будет иметь значение Content как DataContext, которое является экземпляром ProviderViewModel
, полученным из свойства MainWindowViewModel.CurrentViewModel
. ProviderView
теперь унаследует DataContext
от DataTemplate
, то есть упомянутого ProviderViewModel
.
@user1229658 user1229658 Кроме того, если вы добавите MainWindowViewModel
в App.xaml, вам необходимо удалить назначение DataContext из XAML MainWindow
. В противном случае вы создаете два экземпляра этой модели представления. Я почти уверен, что это не то, чего ты хочешь. Вы делали это раньше с помощью ProvoderViewModel.
Вы должны быть осторожны, где и как часто вы создаете объекты XAML.
@BionicCode, ваши комментарии, как обычно, полезны. Вам не нужно явно задавать правильный DataContext, я сделал это только для пояснения, спасибо!
Спасибо @BionicCode, да, это подтверждает мое понимание.
У вас есть две проблемы. Первая проблема — это та, с которой вы обращаетесь за помощью.
ContentControl
не знает, как отображать значение ContentControl.Content
. По умолчанию он вызывает object.ToString
для значения. Если значение не переопределяет object.ToString
, метод возвращает полное имя типа. Это то, что вы видите прямо сейчас. Вы определили DataTemplate, но не применили его. У вас есть два варианта:
а) примените DataTemplate
явно, используя {StaticResource}
:
<ContentControl Content = "{Binding CurrentViewModel}"
ContentTemplate = "{StaticResource g_ProviderViewModel}" />
б) или, альтернативно, удалите x:Key
из элемента DataTemplate
, чтобы сделать его неявным шаблоном. В этом случае WPF автоматически применит шаблон.
Вы создаете два экземпляра MainWindowViewModel
: один в MainWindow
и один в ProviderView
. Общее правило проектирования элементов управления WPF — никогда явно не устанавливать DataContext
внутри элемента управления:
а) Сначала удалите настройку DataContext
из кода XAML ProviderView
.
.
б) ProviderView
теперь будет автоматически наследовать ProviderViewModel
как DataContext
от DataTemplate
. DataTemplate
всегда имеет текущий экземпляр шаблонного типа данных (значение свойства Content
) как DataContext
.
Да, это решает проблему с неотображением
UserControl
, спасибо! Я также обнаружил, что это тоже исправляет:<ContentControl Content = "{Binding CurrentViewModel}" ContentTemplate = "{StaticResource ProviderViewModelTemplate}"/>
, гдеProviderViewModelTemplate
- этоx:Key
дляDataTemplate
изProviderView
. Однако проблема все еще существует. ТеперьUserControl
DataContext
привязан кProviderViewModel
, когда мне нужно, чтобы он был привязан кMainWindowViewModel
, чтобы добраться до экземпляраm_ProviderViewModel
:m_ProviderViewModel property not found on object of type ProviderViewModel.