0
votes

Is there a way to modify DataTemplate before returning it in DataTemplateSelector?

My DataTemplate is defined in XAML. There is an element in this template that I need to set binding for, but whose binding path will only be decided at run-time. The template looks like this:

<DataTemplate DataType="vm:FormField">
  <StackPanel>
    <ComboBox ItemsSource="{Binding ValueList.DefaultView}">
      <ComboBox.ItemTemplate>
        <DataTemplate>
          <TextBlock Text="{Binding Mode=OneWay}" />         <!--This is the problem child-->
        </DataTemplate>
      </ComboBox.ItemTemplate>
    </ComboBox>
  </StackPanel>
</DataTemplate>

TextBlock.Text needs to set its binding path to a property that will be supplied by the underlying data item. My DataTemplateSelector uses the following code to assign it the new path:

public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
  //MultiValueTemplate is the above template
  var Content = MultiValueTemplate.LoadContent() as StackPanel;

  var ComboItemText = (Content.Children[0] as ComboBox).ItemTemplate.LoadContent() as TextBlock;

  //Underlying item contains the field name that I want this to bind to.
  ComboItemText.SetBinding(TextBlock.TextProperty, (item as MyItemVM).FieldName);

  return MultiValueTemplate;
}

This doesn't work. Code runs, but the output doesn't set TextProperty binding. What do I need to change/add?

Note: I have solved this problem using FrameworkElementFactory approach, but I have had to redefine the entire DataTemplate in the code (which is a pain even for simple template like the one above). I want to use the one that I have already defined in XAML.

Note 2: FrameworkElementFactory approach assigns the constructed template object to DataTemplate.VisualTree in the last step, just before returning. I think it is that part that I'm missing, but there is no way of doing that since VisualTree asks for an object of FrameworkElementFactory type, which we do not have when using XAML-based template.

Background

We are basically getting JSON structure from the server-side that looks something like this:

`[ 
   "Person": 
   { 
     "Name": "Peter", 
     "Score": 53000 
   },
   "Person": 
   { 
     "Name": "dotNET", 
     "Score": 24000 
   }
   ,...
 ]

What fields will be included in JSON will be determined by the server. Our application is required to parse this JSON and then display as many ComboBoxes as there are fields. Each ComboBox will then list down one field in it. So in the above example, there will be one combo for Names and one for Scores. User can choose an option either from the first or second ComboBox, but selecting from one combo will automatically select corresponding item from the other combo(s).

Now you may ask, who the hell designed this idiotic UI? Unfortunately we neither know nor control this decision. I ask the client to instead use ONE Combo (instead of many) with a DataGrid as its dropdown, so that we could display one data item per grid row and user could choose one of those items. Clear and Simple. But the management didn't agree and here we are trying to mimic synchronized comboboxes. LOL.

So what we're currently doing is to transform incoming JSON to a DataTable on-the-fly. This DataTable gets one column for each JSON field and as many row as their are items; kind of pivoting you can say. We then create ComboBoes and bind each one to a single field of this DataTable. This field name is of course dynamic and is decided at run-time, which mean that I have to modify the DataTemplate at run-time, which brings up this question.

Hope it didn't get too boring! :)

2
You can construct anything in code-behind that XAML can create. So yes, of course it would be possible for your template selector to return a new template with a binding markup as you want. Your approach above won't work, because the code is modifying the reconstituted StackPanel hierarchy (which is subsequently discarded), not the template that's actually being returned by the method. But that doesn't mean you can't create a new template based on the model and set the binding how you want.Peter Duniho
All that said, it's been my experience that if you find you're trying to head down a path that the WPF API doesn't provide a direct means for, most of the time that means you're going about things the wrong way. There's not enough detail in your question to know how you arrived at this likely XY Problem, nor what the underlying and broader goal is, but there's a good chance the real solution doesn't involve modifying the template at runtime at all.Peter Duniho
@PeterDuniho: Thanks for the input. If you're interested, I have added some background to the question to show how I arrived here.dotNET
There's still no minimal reproducible example in your question. But, seems to me you should just use a single view model class for the template, and then do whatever mapping from database/JSON/server data to the view model in the view model/model logic itself. So far, I don't see any need for the template to be modified. Indeed, the whole point of a view model is to provide a bridge between business logic (like your data returned by the server) and the view (e.g. this template).Peter Duniho

2 Answers

1
votes

look like you can bind SelectedValuePath and DisplayMemberPath to FieldName and be done with that:

<ComboBox SelectedValuePath="{Binding RelativeSource={RelativeSource AncestorType=ComboBox}, Path=DataContext.FieldName}"
          DisplayMemberPath="{Binding RelativeSource={RelativeSource AncestorType=ComboBox}, Path=DataContext.FieldName}"/>
0
votes

Note: For future readers, as mentioned by @ASh in his answer, DisplayMemberPath is a DependencyProperty and can be used to bind to a dynamic field name. This solution in this answer would be over-engineering for this particular problem. I'll still keep it here as it can be useful in certain other scenarios where Binding might not be enough.

Figured it out and was easier than I thought. Instead of modifying the template in DataTemplateSelector, I'm now using a Behavior to modify binding path at runtime. Here is the Behavior:

public class DynamicBindingPathBehavior : Behavior<TextBlock>
{
  public string BindingPath
  {
    get { return (string)GetValue(BindingPathProperty); }
    set { SetValue(BindingPathProperty, value); }
  }

  public static readonly DependencyProperty BindingPathProperty =
    DependencyProperty.Register("BindingPath", typeof(string), typeof(DynamicBindingPathBehavior),
      new FrameworkPropertyMetadata(null, (sender, e) =>
                                          {
                                            var Behavior = (sender as DynamicBindingPathBehavior);
                                            Behavior.AssociatedObject.SetBinding(TextBlock.TextProperty, new Binding(Behavior.BindingPath));
                                          }));
}

And here is the modification that I had to make in my XAML template:

<DataTemplate DataType="vm:FormField">
  <StackPanel>
    <ComboBox ItemsSource="{Binding ValueList.DefaultView}">
      <ComboBox.ItemTemplate>
        <DataTemplate>
          <TextBlock Text="{Binding Mode=OneWay}">
            <e:Interaction.Behaviors>
              <local:DynamicBindingPathBehavior BindingPath="{Binding RelativeSource={RelativeSource AncestorType=ComboBox}, Path=DataContext.FieldName, Mode=OneWay}" />
            </e:Interaction.Behaviors>
          </TextBlock>
        </DataTemplate>
      </ComboBox.ItemTemplate>
    </ComboBox>
  </StackPanel>
</DataTemplate>

All works well from this point forward.

The other approach is to create your template programmatically in your DataTemplateSelector. If you want to go down that route, here is a rough sketch of how to do it in SelectTemplate function:

public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
  var DT = new DataTemplate();

  FrameworkElementFactory stackpanelElement = new FrameworkElementFactory(typeof(StackPanel), "stackpanel");
  FrameworkElementFactory comboboxElement = new FrameworkElementFactory(typeof(ComboBox), "combobox");

  comboboxElement.SetBinding(ComboBox.ItemsSourceProperty, new Binding() { Path = new PropertyPath("ValueList.DefaultView") });
  comboboxElement.SetBinding(ComboBox.SelectedItemProperty, new Binding() { Path = new PropertyPath("Value") });

  var ItemTemplate = new DataTemplate();
  FrameworkElementFactory textblockElement2 = new FrameworkElementFactory(typeof(TextBlock), "textblock2");
  textblockElement2.SetBinding(TextBlock.TextProperty, new Binding() { Path = new PropertyPath(YOUR_BINDING_PROPERTY_PATH) });
  ItemTemplate.VisualTree = textblockElement2;

  comboboxElement.SetValue(ComboBox.ItemTemplateProperty, ItemTemplate);

  stackpanelElement.AppendChild(comboboxElement);
  DT.VisualTree = stackpanelElement;

  return MultiValueTemplate;
}