0

I have a PageViewModelBase where children need to implement 3 methods and everything works as intended, and as such, I'd like to create PageViewBase and have its children implement two simple non-generic things (namely: data context binding and data grid columns).

I've come across this post and tried to create the base xaml accordingly (code below), but despite my different approaches, I'm unable to create the child view. I've tried <baseView:PageViewBase> being the only element in xaml as well as embedding it into another UserControl, but both approaches fail.

Base view (a lot of noise code omitted, added example parts which child view has to implement for visualization):

<UserControl x:Class="WPFapp.Views.Base.PageViewBase">
    <UserControl.Resources>
        <!--<DataTemplate DataType="{x:Type localVM:HardwareViewModel}">
            <local:HardwareView/>
        </DataTemplate>-->
        <localHelpers:DateTimeConverter x:Key="dateTimeConverter" />
        <localHelpers:StatusColorConverter x:Key="statusColorConverter" />
    </UserControl.Resources>
    <UserControl.InputBindings> </UserControl.InputBindings>
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="1*" MinHeight="25" />
            <RowDefinition Height="15*" />
        </Grid.RowDefinitions>
        <ToolBarPanel Grid.Row="0" Orientation="Horizontal">
        </ToolBarPanel>

        <DataGrid ItemsSource="{Binding ItemsList}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" Grid.Row="1" AutoGenerateColumns="False" SelectionMode="Single">
            <DataGrid.ContextMenu> </DataGrid.ContextMenu>
            <DataGrid.InputBindings> </DataGrid.InputBindings>
            <DataGrid.Resources> </DataGrid.Resources>
            <DataGrid.RowStyle> </DataGrid.RowStyle>
            <ContentPresenter ContentSource="Content"/>
            <!--<DataGrid.Columns>
                <DataGridTextColumn Header="LastModifiedBy" Binding="{Binding LastModifiedBy}" />
            </DataGrid.Columns>-->
        </DataGrid>
    </Grid>
</UserControl>

Child view:

<UserControl x:Class="WPFapp.Views.HardwareView">
    <UserControl.Resources>
        <DataTemplate DataType="{x:Type localVM:HardwareViewModel}">
            <local:HardwareView/>
        </DataTemplate>
    </UserControl.Resources>
    <baseView:PageViewBase>
        <DataGrid>
            <DataGrid.Columns>
                <DataGridTextColumn Header="Id" Binding="{Binding Id}" />
                <DataGridTextColumn Header="Type" Binding="{Binding Type}" />
                <DataGridTextColumn Header="Label" Binding="{Binding Label}" />
                <DataGridTextColumn Header="Description" Binding="{Binding Description}" />
                <DataGridTextColumn Header="LastModifiedBy" Binding="{Binding LastModifiedBy}" />
                <DataGridTextColumn Header="LastModifiedAt" Binding="{Binding LastModifiedAt, Converter={StaticResource dateTimeConverter}}" />
            </DataGrid.Columns>
        </DataGrid>
    </baseView:PageViewBase>
</UserControl>

The above child's xaml code is the best I've managed so far, but it ends up displaying columns only (so no toolbar from base, for example) and it does not see UserControl.Resources from base view. How do I make it work?

  • The [DataGrid](https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.datagrid?view=netframework-4.7.2) does not inherit from `ContentControl`, so having a `ContentPresenter` inside a `DataGrid` won't have the effect you want. – redcurry Jan 13 '19 at 17:12
  • Could you explain why you want to define the `DataGrid` columns in another view? Perhaps there's another solution to your problem. – redcurry Jan 13 '19 at 17:16
  • Well, the views are visualizations of my models, and I have 6 different models, for which the handling logic (viewModel) has the very same core (thus they all inherit from `PageViewModelBase`) + some custom implementations for each. Some models have enums, some have DateTimes, some have other classes as their properties. I need to display those in various setups, with various converters. Each of these models is presented as a separate view, but their handling (i.e. hotkeys) and style is consitent with others. That's why I need to specify just the columns (and databinding). – user10835502 Jan 13 '19 at 17:32
  • Not sure exactly what you want to do here. Maybe you could do your common stuff as the template to be applied to a contentcontrol. You then put a datagrid in a contentcontrol in each of your "child" views. Define their columns etc. The commonality is in your template that is applied to that contentcontrol. You could alternatively persuade a usercontrol to take content and again stick your datagrids inside as content. – Andy Jan 13 '19 at 18:21
  • I want to not duplicate dozens of lines of code across 6, and in future many more, views, where the only difference between them are their datagrid columns. As far as I know, templates do not support `InputBindgins` and many other things I have in the code I specified. – user10835502 Jan 13 '19 at 19:04

1 Answers1

0

Here's a solution that uses code-behind to dynamically add the correct columns to the DataGrid. You'll only need the PageViewBase view, so you'll probably want to rename it since it won't be the "base" to anything.

In UserControl.Resources, add all the possible DataGrid columns you can have across all your different models. For example,

<UserControl.Resources>
    <!-- Converters -->
    <localHelpers:DateTimeConverter x:Key="dateTimeConverter" />
    <localHelpers:StatusColorConverter x:Key="statusColorConverter" />

    <!-- DataGrid Columns -->
    <DataGridTextColumn x:Key="IdColumn" Header="Id" Binding="{Binding Id}" />
    <DataGridTextColumn x:Key="TypeColumn" Header="Type" Binding="{Binding Type}" />
    <DataGridTextColumn x:Key="LabelColumn" Header="Number" Binding="{Binding Label}" />
    <DataGridTextColumn x:Key="LastModifiedColumn" Header="LastModifiedAt" Binding="{Binding LastModifiedAt, Converter={StaticResource dateTimeConverter}}" />
    <!-- More column definitions go here for all model types -->
</UserControl.Resources>

Make sure to put the columns after the converters, so that you can use them in data-binding. Now, modify the DataGrid so that it runs the code-behind when it loads. Also, remove the ContentPresenter:

<DataGrid
    ItemsSource="{Binding ItemsList}"
    SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
    Grid.Row="1"
    AutoGenerateColumns="False"
    SelectionMode="Single"
    Loaded="DataGrid_OnLoaded"
    >
    <DataGrid.ContextMenu> </DataGrid.ContextMenu>
    <DataGrid.InputBindings> </DataGrid.InputBindings>
    <DataGrid.Resources> </DataGrid.Resources>
    <DataGrid.RowStyle> </DataGrid.RowStyle>
    <!-- Columns are loaded in code-behind -->
</DataGrid>

In the code-behind, add the correct DataGrid columns depending on the type of view model bound to the specific view (obtained via the view's DataContext):

private void DataGrid_OnLoaded(object sender, RoutedEventArgs e)
{
    if (sender is DataGrid dataGrid)
    {
        if (DataContext is HardwareViewModel)
        {
            dataGrid.Columns.Add(Resources["IdColumn"] as DataGridColumn);
            dataGrid.Columns.Add(Resources["TypeColumn"] as DataGridColumn);
            // More columns added here
        }
        else if (DataContext is AnotherHardwareViewModel)
        {
            dataGrid.Columns.Add(Resources["IdColumn"] as DataGridColumn);
            dataGrid.Columns.Add(Resources["LabelColumn"] as DataGridColumn);
            dataGrid.Columns.Add(Resources["LastModifiedColumn"] as DataGridColumn);
            // More columns added here
        }
    }
}

Finally, whoever is creating the PageViewBase needs to bind it to the desired view model for the specific type you want. For example,

<StackPanel>
    <local:PageViewBase DataContext="{Binding HardwareViewModel}" />
    <local:PageViewBase DataContext="{Binding AnotherHardwareViewModel}" />
</StackPanel>

I've tested this solution for a simple case, and I correctly get different sets of columns in the two DataGrids. However, I haven't tried this with a more complex case, so I apologize in advance if it doesn't solve your specific problem.

redcurry
  • 2,381
  • 2
  • 24
  • 38