UserControl не отображается в WPF ContentControl

Я новичок в 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));
        }
    }
}

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
0
74
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Попробуй это. Заменять:

<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>

Да, это решает проблему с неотображением 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.

user1229658 18.06.2024 22:02

Продолжая мой предыдущий комментарий: я хочу сохранить экземпляр ProviderViewModel «глобальным», потому что его заполнение обходится дорого. По этой причине оно у меня живет в MainWindowViewModel. Кроме того, чтобы переключать представления на основе выбора вкладки, MainWindowViewModel должен управлять экземпляром. В результате я не могу установить DataContext из ProviderView XAML в MainWindowViewModel, потому что это создаст дубликат экземпляра этого класса.

user1229658 18.06.2024 22:05

Добро пожаловать, я опубликовал свой ответ, чтобы получить дополнительную информацию, я проверю и сообщу вам.

Mustafa Mutasim 18.06.2024 22:13

Я хочу знать, правильно ли я вас понимаю, вы хотите MainWindowViewModel.cs только контролировать ProviderViewModel, иначе ProviderViewModel не зависеть от MainWindowViewModel.cs

Mustafa Mutasim 18.06.2024 22:32

Я понял... решение заключалось в том, чтобы добавить App.xaml: <vm:MainWindowViewModel x:Key = "g_MainWindowViewModel" />. А затем укажите это из представлений MainWindow.xaml и ProviderView.xaml в атрибуте управления: DataContext = "{StaticResource g_MainWindowViewModel}"

user1229658 18.06.2024 23:50

@user1229658 user1229658 Вам не нужно явно задавать DataContext для ContentControl. Вы уже установили ProviderViewModel как значение свойства ContentControl.Content. Достаточно. DataTemplate теперь будет иметь значение Content как DataContext, которое является экземпляром ProviderViewModel, полученным из свойства MainWindowViewModel.CurrentViewModel. ProviderView теперь унаследует DataContext от DataTemplate, то есть упомянутого ProviderViewModel.

BionicCode 19.06.2024 00:08

@user1229658 user1229658 Кроме того, если вы добавите MainWindowViewModel в App.xaml, вам необходимо удалить назначение DataContext из XAML MainWindow. В противном случае вы создаете два экземпляра этой модели представления. Я почти уверен, что это не то, чего ты хочешь. Вы делали это раньше с помощью ProvoderViewModel. Вы должны быть осторожны, где и как часто вы создаете объекты XAML.

BionicCode 19.06.2024 00:08

@BionicCode, ваши комментарии, как обычно, полезны. Вам не нужно явно задавать правильный DataContext, я сделал это только для пояснения, спасибо!

Mustafa Mutasim 19.06.2024 00:32

Спасибо @BionicCode, да, это подтверждает мое понимание.

user1229658 19.06.2024 01:02

У вас есть две проблемы. Первая проблема — это та, с которой вы обращаетесь за помощью.

  1. ContentControl не знает, как отображать значение ContentControl.Content. По умолчанию он вызывает object.ToString для значения. Если значение не переопределяет object.ToString, метод возвращает полное имя типа. Это то, что вы видите прямо сейчас. Вы определили DataTemplate, но не применили его. У вас есть два варианта:
    а) примените DataTemplate явно, используя {StaticResource}:

    <ContentControl Content = "{Binding CurrentViewModel}"
                    ContentTemplate = "{StaticResource g_ProviderViewModel}" />
    

    б) или, альтернативно, удалите x:Key из элемента DataTemplate, чтобы сделать его неявным шаблоном. В этом случае WPF автоматически применит шаблон.

  1. Вы создаете два экземпляра MainWindowViewModel: один в MainWindow и один в ProviderView. Общее правило проектирования элементов управления WPF — никогда явно не устанавливать DataContext внутри элемента управления:

    а) Сначала удалите настройку DataContext из кода XAML ProviderView.
    . б) ProviderView теперь будет автоматически наследовать ProviderViewModel как DataContext от DataTemplate. DataTemplate всегда имеет текущий экземпляр шаблонного типа данных (значение свойства Content) как DataContext.

Другие вопросы по теме