Я новичок в 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. Однако проблема все еще существует. ТеперьUserControlDataContextпривязан кProviderViewModel, когда мне нужно, чтобы он был привязан кMainWindowViewModel, чтобы добраться до экземпляраm_ProviderViewModel:m_ProviderViewModel property not found on object of type ProviderViewModel.