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! :)
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}"/>
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;
}
Related
I'm building a control that can edit POCOs. There is a descriptor collection for the fields within the POCO that need to be edited and I'm binding a ListBox's ItemsSource to this collection. Amongst other things, the descriptor gives me the ability to select a suitable DataTemplate and the variable name in the POCO that this ListBox item should edit.
My ListBox is built like this:
<ListBox ItemsSource="{Binding ColumnCollection, ElementName=root}">
<ListBox.Resources>
<DataTemplate x:Key="TextTemplate">
<StackPanel>
<TextBlock Text="{Binding DisplayName}" />
<!-- !!! Question about following line !!! -->
<TextBox Text="{Binding ElementName=vm.CurentEditing, Path=PathName}" />
</StackPanel>
</DataTemplate>
<!-- Details omitted for brevity -->
<DataTemplate x:Key="PickListTemplate" />
<DataTemplate x:Key="BooleanTemplate" />
</ListBox.Resources>
<ListBox.ItemTemplateSelector>
<local:DataTypeSelector
TextTemplate="{StaticResource TextTemplate}"
PickListTemplate="{StaticResource PickListTemplate}"
BooleanTemplate="{StaticResource BooleanTemplate}"
/>
</ListBox.ItemTemplateSelector>
</ListBox>
It is the TextBox binding expression in the "TextTemplate" that I am having problems with. The problem is that "PathName" should not be taken as a literal string, but is the name of a string property in the ColumnDescription class (the collection type of ColumnCollection used for ListBox.ItemsSource), which gives the name of the POCO property I want to bind to (the POCO is "vm.CurrentEditing").
Is there some way to use the value of a property in XAML as input to a binding expression, or will I have to resort to code behind?
(Incidentally, specifying the ElementName as "x.y" as I have done above also seems to be invalid. I assume the "y" part should be in Path but that's currently taken up with my property name...!)
So you want to bind TextBox.Text to Property X of Object Y, where X and Y both change at runtime.
It sounds like what you want to do is something analogous to ListBox.DisplayMemberPath: You can bind a string or PropertyPath property to DisplayMemberPath and it'll work. The way I've done stuff like that is to have a dependency property of type String or PropertyPath, and programatically create a binding from that to whatever property.
So, I wrote an attached property which creates a binding.
public class POCOWrangler
{
#region POCOWrangler.BindPropertyToText Attached Property
public static String GetBindPropertyToText(TextBox obj)
{
return (String)obj.GetValue(BindPropertyToTextProperty);
}
public static void SetBindPropertyToText(TextBox obj, PropertyPath value)
{
obj.SetValue(BindPropertyToTextProperty, value);
}
public static readonly DependencyProperty BindPropertyToTextProperty =
DependencyProperty.RegisterAttached("BindPropertyToText", typeof(String), typeof(POCOWrangler),
new PropertyMetadata(null, BindPropertyToText_PropertyChanged));
private static void BindPropertyToText_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue is String && d is TextBox)
{
var tb = d as TextBox;
var binding = new Binding((String)e.NewValue);
// The POCO object we're editing must be the DataContext of the TextBox,
// which is what you've got already -- but don't set Source explicitly
// here. Leave it alone and Binding.Source will be updated as
// TextBox.DataContext changes. If you set it explicitly here, it's
// carved in stone. That's especially a problem if this attached
// property gets initialized before DataContext.
//binding.Source = tb.DataContext;
binding.Mode = BindingMode.TwoWay;
BindingOperations.SetBinding(tb, TextBox.TextProperty, binding);
}
}
#endregion POCOWrangler.BindPropertyToText Attached Property
}
And I wrote a quick example thing: There's a little class named Foo that has a Name property, and a viewmodel with two properties, Foo Foo and String DisplayPathName. It works! Of course, this depends on default TextBox editing behavior for whatever type the property happens to be. I think that will get you the same results as if you'd bound explicitly in XAML, but it sitll won't always necessarily be just what you want. But you could very easily go a little nuts and add some triggers in the DataTemplate to swap in different editors, or write a DataTemplateSelector.
I stuffed ViewModel.Foo in a ContentControl just to get a DataTemplate into the act, so that the TextBox gets his DataContext in the same manner as yours.
Note also that I'm getting DisplayPathName by a relative source from something outside the DataContext object -- it's not a member of Foo, of course, it's a member of the viewmodel.
C#
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel {
DisplayPathName = "Name",
Foo = new Foo { Name = "Aloysius" }
};
}
XAML
<ContentControl
Content="{Binding Foo}"
>
<ContentControl.ContentTemplate>
<DataTemplate>
<TextBox
local:POCOWrangler.BindPropertyToText="{Binding
DataContext.DisplayPathName,
RelativeSource={RelativeSource AncestorType=ContentControl}}"
/>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
That was fun.
First off, an apology as I am sure this is question's answer is quite simple. Nonetheless I still cannot seem to find an answer.
This is my first week using WPF. I simply wish to display the contents of some sort of list inside of a DataGrid. I am currently trying to use an ObservableCollection<> and a DataGrid, but that can change. How do I DataTemplate the list and display it?
The list is in a ViewModel, which has been set in Apps.xaml.cs as the DataSource for the MainWindow.xaml:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var window = new MainWindow();
// Create the ViewModel to which
// the main window binds.
var viewModel = new MainWindowViewModel();
// Allow all controls in the window to
// bind to the ViewModel by setting the
// DataContext, which propagates down
// the element tree.
window.DataContext = viewModel;
window.Show();
}
Here is the current list:
public ObservableCollection<PersonData> SearchResults { get; set; }
As for my xaml, though, I am rather lost -- I've tried fiddling around with binding and ItemsSource, and really have no idea how to use them here. Also, I suppose using a DataTemplate will be necessary, as I need to let it know somehow that the columns need to display certain properties of the PersonData, such as first and last name, location, and title. Any help is appreciated.
EDIT
More generally -- how does one simply display a ViewModel list, collection, what have you, period?
Your basic DataGrid syntax should look something like this:
<DataGrid ItemsSource="{Binding SearchResults}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="First Name" Binding="{Binding FirstName}"/>
<DataGridTextColumn Header="Last Name" Binding="{Binding LastName}" />
</DataGrid.Columns>
</DataGrid>
If you don't want to display your data in a DataGrid, you can use an ItemsControl.
The default ItemsControl will add all your items to a StackPanel, and draw each of them using a TextBlock bound to the .ToString() of the item. You can change how the ItemsControl displays the items by specifying your own ItemsPanelTemplate and ItemTemplate for it to use. For some examples, check out my blog post on WPF's ItemsControl.
Ach. Failure to pre-study is fatal -- I did not implement INotifyPropertyChanged. Found out how to here: http://msdn.microsoft.com/en-us/library/ms743695
I try to implement PrepareContainerForItemOverride method of ItemsControl. It will put items to TextBox. It works nice, but how can I binding an item to the textbox text property? One way mode works nice, but when I want two way mode, I have to know the path.
Here is my code:
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
if (element is TextBox)
{
//((TextBox)element).Text = (string)item;
Binding binding = new Binding("I don't know what should i write here.");
binding.Mode = BindingMode.TwoWay;
((TextBox)element).SetBinding(TextBox.TextProperty, binding);
}
}
Thank you for your help!
If the commented line in the code in your question is what you have before then it indicates that the type of item you are providing is String. Two way binding on a string makes no sense the binding would not know where to assign the new value.
The type of items being displayed would need to be some object that has a property of type String, it would be the name of this proprerty that you pass to the Binding constructor.
That said its not clear why you would even need to sub-class ItemsControl in this way. Why not:-
<ItemsControl ItemSource="{Binding SomeEnumberableOfObjectsThatHaveASomeStringProperty}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Test="{Binding SomeString, Mode=TwoWay}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
A simple WPF ListView:
<ListView Name="recordContainer" ItemsSource="{Binding Path=MyCollection}">
<GridView>
<GridViewColumn Width="260" Header="Name" DisplayMemberBinding="{Binding Path=Name}"/>
<GridViewColumn Width="100" Header="Value" DisplayMemberBinding="{Binding Path=Value}"/>
</GridView>
</ListView>
MyCollection is a property on my Page:
public ObservableCollection<MyData> MyCollection
{
get
{
return myCollection;
}
}
and this is my data object:
public class MyData
{
public string Name { get; set; }
public string Value { get; set; }
}
I fill up myCollection in the contructor of the page (before InitializeComponent) but the list is coming up empty!
I have the exact same configuration on other pages and it works fine - what am I missing?
Any help appreciated!
Set the DataContext of the page to the page itself :
this.DataContext = this;
What Thomas said... or...
What you're missing is that the binding is actually examining the DataContext. You can set the source differently a number of ways, however.
Think of it this way... Where is the binding to look for MyCollection? Binding is just a class; it isn't all knowing. You have to tell it where to look. By default, this is the DataContext. The DataContext is shared among the logical tree elements of your UI, with items lower in the tree able to see DataContexts higher up in the tree, or even override this value for items lower than themselves.
In your case, you want a value located on your Page which is not the DataContext. You must tell Binding how to find this. You can do this via the RelativeSource property.
{Binding MyCollection RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Page}}}
This is a combination of three markup helpers, Binding, RelativeSource, and Type.
RelativeSource finds an object in the tree relative to the current position. In this case, we are Finding an Ancestor (we're looking up the tree). RelativeSource walks up the tree looking for the first object of type Page. Once it finds this, it returns it to Binding. Binding then examines this object for a property MyCollection.
Here: http://simplesample.site90.com/wpf_binded_listview.php
is a complete example showing a ListView binded to a ObservableCollections and manipulating them using Commands.
Hope this helps.
I believe the problem is that a listview takes as a container something like a gridview. This is how a listview allows for non-linear arrangements of items unlike a listbox.
Think about it as
ListView = Layout Coordinator
GridView = Layout Style
Elements = Items to be displayed
Unless you intend to display your items in anyway other than a list, use a ListBox.
(p.s. disregard highlighting)
I'm trying to bind two ListBoxes:
<ListBox SelectionChanged="lbApplications_SelectionChanged"
ItemsSource="{Binding Path=Applications,
UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" />
<ListBox DisplayMemberPath="Message"
ItemsSource="{Binding Path=Events,
UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" />
Applications and Events are public properties in Window class.
I set DataContext to this to both list boxes and implement INotifyPropertyChanged in Window class:
private void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
And then after adding new item to Applications or Events I call:
NotifyPropertyChanged("Events");
NotifyPropertyChanged("Applications");
The issue is that ListBox is loaded only one time. What am I doing wrong?
Let's just look at one of the ListBoxes, since they're both the same, basically.
The code we're concerned about is this:
<ListBox ItemsSource="{Binding Path=Applications,
UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" />
Since you're new to WPF, let me say you probably don't need UpdateSourceTrigger or Mode in there, which leaves us with this:
<ListBox ItemsSource="{Binding Path=Applications}" />
You mentioned that Applications is a public property in your code-behind. You need it to be a DependencyProperty, and you need it to fire events when it changes -- most people use an ObservableCollection for this.
So your code-behind will have something like this:
public ObservableCollection<string> Applications
{
get { return (ObservableCollection<string>)GetValue(ApplicationsProperty); }
set { SetValue(ApplicationsProperty, value); }
}
public static readonly DependencyProperty ApplicationsProperty =
DependencyProperty.Register("Applications",
typeof(ObservableCollection<string>), typeof(Window1),
new UIPropertyMetadata(null));
Then, where you want to add it, you'll do something like this:
this.Applications = new ObservableCollection<string>();
Applications.Add("Whatever");
Finally, for the "simple" binding syntax to work in the XAML, I usually change the DataContext in my Window (or the root Control element for the file, whatever I'm working in) to
<Window DataContext="{Binding RelativeSource={RelativeSource Self}}" ... >
...
Your Applications box will update automatically.
The problem is that your property value hasn't changed. It's still the same list, same reference.
One solution might be that your collections are of type ObservableCollection. These lists provide events for WPF when you add or remove items.