3

I am new to WPF and MVVM. I've spent two days scrolling through a bunch of SO q&a's on this topic and I haven't had any success getting this to work. Which inevitably means it's something silly that I'm overlooking.

I am trying to use DataTemplate along with a ContentControl to switch between content DataGrids as a user changes tabs in a Fluent:Ribbon control. I would like to reuse the views, as the DataGrids contained therein can be expensive to fill.

I have these views/viewmodels:

  • MainWindow.xaml / MainWindowViewModel.cs - the main application window consisting of Fluent:RibbonWindow, Fluent:Ribbon and Fluent:StatusBar controls (some of which are removed in my code snippets below for clarity). This class contains a member property for tracking "current content" (CurrentViewModel) and member properties for command processing when user clicks on buttons in the Fluent:Ribbon. Other viewmodels are instantiated in this class as private members.
  • ProviderView.xaml / ProviderViewModel.cs - displays a list of "Providers" (for the purposes of this post, just an abstract concept). The view contains a UserControl that contains a DataGrid control. The DataGrid is bound to the public Providers property (a list of Provider objects) inside an instance of ProviderViewModel which is public property of MainWindowViewModel.

When I run the application, and also apparent in the designer, the ContentControl just contains a string ViewModel.ProviderViewModel, as if it has no idea what to do with the control, or I'm not treeing it properly.

enter image description here

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));
        }
    }
}

2 Answers 2

4

You have two problems. The first problem is the one you are seeking help with.

  1. The ContentControl does not know how to render the ContentControl.Content value. By default, it will call the object.ToString on the value. Unless the value does not override object.ToString, the method returns the fully qualified type name. That's what you are seeing right now. You have defined a DataTemplate, but never applied it. You have two options:
    a) apply the DataTemplate explicitly using {StaticResource}:

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

    b) or alternatively remove the x:Key from the DataTemplate element to make it an implicit template. In this case WPF will automatically apply the template for you.

  1. You are creating two instances of MainWindowViewModel: one in MainWindow and one in ProviderView. A common WPF control design rule is to never explicitly set the DataContext from inside a control:

    a) First remove the DataContext setting from the XAML code of ProviderView.
    b) ProviderView will now automatically inherit the ProviderViewModel as DataContext from the DataTemplate. The DataTemplate always has the current instance of templated data type (the value of the Content property) as DataContext.

Sign up to request clarification or add additional context in comments.

Comments

2

Try this. Replace:

<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="1">
    <ContentControl Content="{Binding CurrentViewModel}"/>
</StackPanel>

With:

<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="1">
    <local:ProviderView DataContext="{Binding CurrentViewModel}"/>
</StackPanel>

Remove from ProviderView.xaml:

<UserControl.DataContext><local:MainWindowViewModel/></UserControl.DataContext>

9 Comments

Yes, this fixes the issue of the UserControl not showing, thanks! I also discovered this fixes it as well: <ContentControl Content="{Binding CurrentViewModel}" ContentTemplate="{StaticResource ProviderViewModelTemplate}"/>, where ProviderViewModelTemplate is the x:Key for the DataTemplate of the ProviderView. However, there is still an issue. Now the UserControl's DataContext is bound to ProviderViewModel, when I need it to be bound to MainWindowViewModel to get at m_ProviderViewModel instance: m_ProviderViewModel property not found on object of type ProviderViewModel.
To build on my prior comment: I want to keep the instance of ProviderViewModel "global", because it's expensive to populate. For this reason, I have it living in MainWindowViewModel. Also, in order to switch views based on tab selection, MainWindowViewModel must control the instance. As a result, I cannot set DataContext from ProviderView XAML to MainWindowViewModel, because it will create a duplicate instance of that class.
I figured it out.. solution was to add in App.xaml: <vm:MainWindowViewModel x:Key="g_MainWindowViewModel" />. And then reference this from the views MainWindow.xaml and ProviderView.xaml in the control attribute: DataContext="{StaticResource g_MainWindowViewModel}"
@user1229658 You don't have to explicitly set the DataContext of the ContentControl. You already set the ProviderViewModel as property value of the ContentControl.Content property. That's enough. The DataTemplate will now have the Content value as DataContext, which is the ProviderViewModel instance received from the MainWindowViewModel.CurrentViewModel property. The ProviderView will now inherit the DataContext of the DataTemplate, which is the mentioned ProviderViewModel.
@user1229658 Also, if you add MainWindowViewModel to App.xaml you must remove the DataContext assignment from the XAML of MainWindow. Otherwise, you are creating two instances of this view model. I'm pretty sure this is not what you want. You did this before with the ProvoderViewModel. You must be careful where and how often you create your XAML objects.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.