0

I have a chunk of xaml that duplicates the same pattern six times and would like to reduce that footprint by eliminating the duplication. I'm running into one snag that I need help with.

Background: I have a class that instantiates another class six times (phases of a project as you will).

   public ECN(string ecnNumber) {
        _ECNNumber = ecnNumber;

        //instantiate each phase to be populated or not
        _PurchaseParts = new ECNPhase();
        _PieceParts = new ECNPhase();
        _Weldments = new ECNPhase();
        _BOMCfg = new ECNPhase();
        _Cleanup = new ECNPhase();
        _PRAF = new ECNPhase();   
    }

Inside each phase is a collection of Changes (another class) referenced in the ECNPhase Class. Each phase has data that is unique to each phase that is shown in a unique view, this is where my snag is which I will show later.

Example of the duplicate xaml Code with the main difference being the different view inside each expander:

<StackPanel Margin="0">

    <!--Section for Purchase parts-->
    <StackPanel Orientation="Horizontal" >
        <CheckBox Margin="0,5,5,5" IsChecked="{Binding Path=MyWorkspace.CurrentSelectedItem.PurchaseParts.HasPhase,Mode=TwoWay}"/>
        <StackPanel Orientation="Horizontal">
            <StackPanel.Style>
                <Style TargetType="{x:Type StackPanel}">
                    <Setter Property="IsEnabled" Value="False"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding Path=MyWorkspace.CurrentSelectedItem.PurchaseParts.HasPhase}" Value="True">
                            <Setter Property="IsEnabled" Value="True"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </StackPanel.Style>
            <Expander Header="Purchase Parts" Margin="0,0,10,0" Width="110">
                <view:PurchasePartsView/>
            </Expander>
            <CheckBox Content="Submit" Margin="10,5,0,5"/> <!--add a command to handle the submit checkbox event-->
            <Label Content="Status:" Margin="10,0,0,0" HorizontalContentAlignment="Right" Width="60"/>
            <Label Content="{Binding Path=MyWorkspace.CurrentSelectedItem.PurchaseParts.Status}"/>
        </StackPanel>
    </StackPanel>

    <!--Section for Piece Parts-->
    <StackPanel Orientation="Horizontal">
        <CheckBox  Margin="0,5,5,5" IsChecked="{Binding Path=MyWorkspace.CurrentSelectedItem.PieceParts.HasPhase,Mode=TwoWay}"/>
        <StackPanel Orientation="Horizontal">
            <StackPanel.Style>
                <Style TargetType="{x:Type StackPanel}">
                    <Setter Property="IsEnabled" Value="False"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding Path=MyWorkspace.CurrentSelectedItem.PieceParts.HasPhase}" Value="True">
                            <Setter Property="IsEnabled" Value="True"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </StackPanel.Style>
            <Expander Header="Piece Parts"  Margin="0,0,10,0" Width="110">
                <view:PiecePartsView/>
            </Expander>
            <CheckBox Content="Submit" Margin="10,5,0,5"/> <!--add a command to handle the submit checkbox event-->
            <Label Content="Status:" Margin="10,0,0,0" HorizontalContentAlignment="Right" Width="60"/>
            <Label Content="{Binding Path=MyWorkspace.CurrentSelectedItem.PieceParts.Status}"/>
        </StackPanel>
    </StackPanel>
    <!--duplicated four more times-->
</StackPanel>

What I'd like to do is:

<StackPanel>
    <view:PhaseView DataContext="{Binding Path=MyWorkspace.CurrentSelectedItem.PurchaseParts}"/>
    <view:PhaseView DataContext="{Binding Path=MyWorkspace.CurrentSelectedItem.PieceParts}"/>
    <!--four more phases-->
</StackPanel>

Where the PhaseView will be the template that handles the duplication and this is where I'm hitting a snag. Each phase needs a unique view (userControl) selected based off of the datacontext of the PhaseView.

<StackPanel Orientation="Horizontal" >
    <CheckBox Margin="0,5,5,5" IsChecked="{Binding Path=HasPhase,Mode=TwoWay}"/>
    <StackPanel Orientation="Horizontal">
        <StackPanel.Style>
            <Style TargetType="{x:Type StackPanel}">
                <Setter Property="IsEnabled" Value="False"/>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Path=HasPhase}" Value="True">
                        <Setter Property="IsEnabled" Value="True"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </StackPanel.Style>
        <Expander Header="DisplayName" Margin="0,0,10,0" Width="110">
            <!--add somthing here to select the correct view based on the datacontext-->
            <!--<local:PurchasePartsView/>  This user control adds a datagrid that is unique to each phase-->
        </Expander>
        <CheckBox Content="Submit" Margin="10,5,0,5"/> <!--add a command to handle the submit checkbox event-->
        <Label Content="Status:" Margin="10,0,0,0" HorizontalContentAlignment="Right" Width="60"/>
        <Label Content="{Binding Path=Status}"/>
    </StackPanel>
</StackPanel>

I was thinking of using a datatrigger somehow lik shown below, but I haven't had any luck figuring it out. I know there's got to be a way to do this, and it's probably something simple and dumb. Any help would be much appreciated.

<DataTrigger Binding="{Binding Path=DisplayName}" Value="Purchase Parts">
   <Setter Property="DataContext" Value="{Binding }"/> <!--Don't know how to bind the DataContext-->
</DataTrigger>

Thanks,

R. Roe
  • 609
  • 7
  • 18
  • 1
    There are 3 options. Create a `DataTemplateSelector` to do it programatically, reate `DataTemplates` without a `Key` in `Resources`, or use `Triggers`. My personal preference is to place a `DataTemplate` for each type in to `Resources` and let WPF pick the correct one automatically. Here is an [Example](https://stackoverflow.com/questions/35493146/selecting-a-data-template-based-on-type). – Bradley Uffner Sep 08 '17 at 17:54
  • The only issue i have is that the class is instantiated several times. So if I use the `DataTemplate` it would look like `DataType="Local:ECNPhase"` which doesn't help me select the different instantiates of the same class. – R. Roe Sep 08 '17 at 18:09
  • 1
    The [`DataTemplateSelector`](https://msdn.microsoft.com/en-us/library/system.windows.controls.datatemplateselector%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396) is probably what you want then. It has the most flexibility. It will let you select a `Template` not only by `Type`, but also by data contained within the `DataContext` instance for the current item. – Bradley Uffner Sep 08 '17 at 18:11
  • 1
    [This](https://learn.microsoft.com/en-us/dotnet/framework/wpf/data/data-templating-overview#choosing-a-datatemplate-based-on-properties-of-the-data-object) is another good read about changing the `DataTemplate` based on data within `DataContext`. – Bradley Uffner Sep 08 '17 at 18:20

1 Answers1

0

Ok, thanks to Bradley I looked into the DataTemplateSelector and this is what I came up with.

In my UserControl resources I set up several DataTemplates and a reference to the TemplateSelector class that overides the DataTemplateSelector class.

XAML Resources:

<UserControl.Resources>

    <local:TemplateSelector x:Key="myTemplateSelector"/>

    <DataTemplate x:Key="PurchasePartsTemplate">
        <view:PurchasePartsView/>
    </DataTemplate>
    <DataTemplate x:Key="PiecePartsTemplate">
        <view:PiecePartsView/>
    </DataTemplate>
    <!--Four more templates-->        
</UserControl.Resources>

Code Behind for DataTemplateSelector override. Note: I couldn't figure out a way to bind to the ECNPhase class so I bound to the DisplayName property in my class to pull out the correct instance being represented.

class TemplateSelector : DataTemplateSelector {

    public override DataTemplate SelectTemplate(object item, DependencyObject container) {

        FrameworkElement element = container as FrameworkElement;

        if(element != null && item != null && item is string) {
            string phase = (string)item;

            if(phase == "Purchase Parts") {
                return element.FindResource("PurchasePartsTemplate") as DataTemplate;
            }else if(phase == "Piece Parts") {
                return element.FindResource("PiecePartsTemplate") as DataTemplate;
            }
        }
        return null;
    }
}

I'm calling this class in my UserContol like this:

<Expander Header="{Binding Path=DisplayName}" Margin="0,0,10,0" Content="{Binding Path=DisplayName}" 
                  ContentTemplateSelector="{StaticResource myTemplateSelector}"/>

There isn't an items control associated with the expander so I used the content control. I pass the DisplayName into the control property and the contentTemplateSelector uses the myTemplateSelector resource which goes into the codebehind and selects the appropriate datatemplate to use based on the DisplayName.

Now I can call my reusable template like so:

<StackPanel Margin="0">
    <view:ChangePhaseView DataContext="{Binding Path=MyWorkspace.CurrentSelectedItem.PurchaseParts}"/>
    <view:ChangePhaseView DataContext="{Binding Path=MyWorkspace.CurrentSelectedItem.PieceParts}"/>
</StackPanel>

@Bradley, thank you for pointing me in the right direction.

R. Roe
  • 609
  • 7
  • 18