Is there a way to use a DataTemplate with a parameter? - wpf

I need to dynamically add/remove GridView columns, each displaying information from a different element stored in a KeyedCollection (indexed with tn 'int'). The basic technique works, but requires an index, as follows:
<GridViewColumn Header="bid">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Width="60" DataContext="{Binding Elements}" Text="{Binding [546].PropName}" TextAlignment="Center" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
However, at run-time I need to add more of these, with different integer keys, at which point I'm not sure how to create new DataTemplates, each with a different binding index.
Constructing a new DataTemplate using the XamlParser seems quite ugly...
Any help?

Well, from what I seem to understand you need your objects to have some additional properties. Something like Key and ValueFromKey. This properties could look similar to this:
public int Key { get; set; }
public object ValueFromKey
{
get { return this[Key]; }
}
Then at the moment you're adding the GridView columns you should set the Key property's value to the 'magic' number (like 546 from the example).
And your DataTemplate will look as simple as this:
<DataTemplate>
<TextBlock
Width="60"
Text="{Binding ValueFromKey.PropName}"
TextAlignment="Center"
/>
</DataTemplate>
The problem arises if you cannot change that class. Then you could probably consider wrapping it with your own class (kind of a ViewModel) and bind your UI to a collection of those new instances.

Related

How to bind in WPF with values upstream in hierarchy?

I have a list of items I wish to display in a ListView/GridView. Each item is a class/object containing a format and array of bytes. The format dictates how the bytes are to be displayed (hex or decimal). I am using a converter to go back and forth between TextBox.Text and the byte array.
The converter needs the format and string/array. I tried to use IValueConverter and pass the format as a ConverterParameter, but this didn't work since it is not a DependencyProperty. I tried to use IMultiValueConverter but that didn't work because I do not get the format in ConvertBack. I thought that if I could bind to the whole object (MyDataItem), then the converter would work fine. However, I cannot figure out how to bind to that. I tried a bunch of variations using RelativeSource and other properties, but couldn't figure it out. Can someone help me with the binding?
If there is a better way to accomplish my task, feel free to suggest it.
public enum FormatEnum
{
Decimal,
Hex
}
public class MyDataItem
{
public byte[] Data { get; set; }
public FormatEnum Format { get; set; }
}
public class ViewModel
{
ObservableCollection<MyDataItem> DataItems = new ObservableCollection<MyDataItem>();
}
XAML (with non-working binding)
<ListView ItemsSource="{Binding DataItems}">
<ListView.View>
<GridView>
<GridViewColumn Header="Format">
<GridViewColumn.CellTemplate>
<DataTemplate>
<!--ComboBox for the format-->
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Data">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Path,
Converter={StaticResource ResourceKey=DataBytesConverter},
ConverterParameter={Binding Format}}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
Welcome to SO!
Just a heads-up for future....you'll have a much better chance of getting questions answered if you provide an MCVE. The more work you make people do to reproduce your exact problem, the less inclined they'll be to do so.
There are a couple of different ways of doing what you're trying to do, but the main problem is that the data you are binding to doesn't support INPC. I'll let you read up on that, but the problem is essentially that the left hand (your text display field) doesn't know what your right hand (the ComboBox) is doing. When the ComboBox changes the format it has to signal to anything that depends on it that the value has changed. That's where INPC comes in. You can implement it yourself if you like, and there are plenty of tutorials around the net showing how to do so, but it's much easier to just use an existing library like MVVM Light, which is what I'll use here.
Before I go into detail, I should point out that your idea of using an IMultiValueConverter would in fact work, provided you passed in Data and Format as separate parameters so that it would update whenever either of them changes value. Many people will actually suggest solutions like this, but it's not the best way of doing what you're trying to achieve. Converters and behaviors are really just extensions of the view, which is fine when they expose parts of the view that you otherwise can't get to. But in your case the problem is that the data you are providing the view isn't in a format that your view can readily consume, and in a "proper" WPF application, it should be. Fixing your data beforehand is usually faster, a lot easier to debug when it goes wrong, and it opens up the possibility for things like unit testing. We already know that your MyDataItem class has to be either modified or replaced in order to support INPC, so that's a good place to do your values-to-text logic.
So to start with, create a view model for your model object that exposes the properties you want to pass to your view laye (i.e. "Format") and adds a new one for the text string you want to display (i.e. "DataText). We'll also create an update function to fill that string in, INPC support and a tiny bit of update logic:
public class MyDataItemViewModel : ViewModelBase
{
private MyDataItem DataItem;
public MyDataItemViewModel(MyDataItem dataItem)
{
this.DataItem = dataItem;
UpdateDataText();
}
public FormatEnum Format
{
get { return this.DataItem.Format; }
set
{
if (this.DataItem.Format != value)
{
this.DataItem.Format = value;
RaisePropertyChanged(() => this.Format);
UpdateDataText();
}
}
}
private string _DataText;
public string DataText
{
get { return this._DataText; }
set
{
if (this._DataText != value)
{
this._DataText = value;
RaisePropertyChanged(() => this.DataText);
}
}
}
private void UpdateDataText()
{
switch (this.Format)
{
case FormatEnum.Decimal:
this.DataText = String.Join(", ", this.DataItem.Data.Select(val => String.Format("{0:D}", val)));
break;
case FormatEnum.Hex:
this.DataText = String.Join(", ", this.DataItem.Data.Select(val => String.Format("0x{0:X2}", val)));
break;
default:
this.DataText = String.Empty;
break;
}
}
}
Your "DataItems" collection needs to be public and accessible via a getter (something you didn't do in your original code), and it needs to be a collection of these view models instead:
public ObservableCollection<MyDataItemViewModel> DataItems { get; } = new ObservableCollection<MyDataItemViewModel>();
And now for each instance of MyDataItem you want to wrap it in an instance of MyDataItemViewModel:
this.DataItems.Add(new MyDataItemViewModel(new MyDataItem { Format = FormatEnum.Decimal, Data = new byte[] { 1, 2, 3 } }));
this.DataItems.Add(new MyDataItemViewModel(new MyDataItem { Format = FormatEnum.Decimal, Data = new byte[] { 4, 5, 6 } }));
Now your data is in a much better format. ObjectDataProvider provides a nice and convenient way to put all the values for a particular type of Enum in a single list that a ComboBox can bind to, so let's create one of those for your FormatEnum:
xmlns:system="clr-namespace:System;assembly=mscorlib"
<Window.Resources>
<ObjectDataProvider x:Key="FormatEnumValues" MethodName="GetValues" ObjectType="{x:Type system:Enum}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:FormatEnum"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
And your ListView can now bind directly to all this data without any converters or behaviors or anything messy like that:
<ListView ItemsSource="{Binding DataItems}">
<ListView.View>
<GridView>
<GridViewColumn Header="Format">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Source={StaticResource FormatEnumValues}}" SelectedItem="{Binding Path=Format}" Width="100" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Data">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding DataText}" TextTrimming="CharacterEllipsis" MinWidth="100" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
Result:

What is normally bound to a wpf combobox

I want to bind something to a combobox that will display something different than it's value.
What would be the suggested/most common data container to link the combobox to? Would it be a custom type and then do a list of that type? A data table? I am using vb.net and wpf. The list would be something like:
dog,1
cat,2
bird,3
fish,4
The combobox would display the animal name and the value would be the number. The data to populate the combobox will come from a MYSql database.
Set the DisplayMemberPath on your Combobox to the property of the underlying object you want to
display.
So assuming you have an object like this:
Public Class Animal
{
string Name {get;set;}
int Number {get;set;}
}
You would set your DisplayMemberPath to Name.
Else check out this link for a more complete answer:
Binding WPF ComboBox to a Custom List
You can populate with anything you want, you can also display multiple items like so
<ComboBox ItemsSource{Binding ItemList}>
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding AnimalName}" />
<Checkbox IsChecked={Binding SelectAnimal}" Content="{Binding Age}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
The Select animal is just so you can get an idea of doing a template

silverlight 4 {binding} generic dictionary to listbox (items not being displayed)

My xaml ...
<ListBox Margin="6,35,6,6" Name="lbLayers" SelectionMode="Extended" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Key,Mode=TwoWay}" />
<TextBlock Text="{Binding Value.Visible,Mode=TwoWay,StringFormat='Visible: {0}'}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
.. and my code is ...
void GameEvents_MapCreated(object sender, Zider.EventArgs.MapEventArgs e)
{
HookMapLayerEvents(false);
this.map = e.Map;
HookMapLayerEvents(true);
this.lbLayers.ItemsSource = this.map.Layers;
}
this.map.layers is a generic dictionary of type (string, MapLayer(Tile))
When I set ItemSource on the listbox there are no items in the dictionary to start with. When I click a button that is when I add a map layer to
this.map.Layers.Add("key", new MapLayer<Tile>());
Also MapLayer implements INotifyPropertyChanged for it's properties.
For the life of me I can't seem to get the items to be displayed in the listbox.
The problem is that the value of map.Layers doesn't change nor does Dictionary<TKey, TValue> implement the INotifyCollectionChanged interface. Hence there is no way for the ListBox to know that any new item is available to display.
If possible try change the Layers property so that it exposes a ObservableCollection<T> instead.
Of course that could be a problem if you must have a dictionary. If you are only interested in ensuring Unique entries you could use a HastSet of the keys to manage that. If you need the Key for look up then if there are few items a sequential search should do reasonably well.
A full blown solution might be to implement an ObservableDictionary that has both IDictionary<TKey, TValue> and INotifyCollectionChanged interfaces. There are a few about if you search for "ObservableDictionary Silverlight", just be careful that they actually implement the correct interfaces, its not good if its "observable" but not in a way compatable with ItemsSource.

Silverlight listbox question

I'm using listBox.ItemsSource = e.Result.Persons, which is a collection of persons. The listbox shows the actual object names when I would like it to show the first name of each person object. How can I do this?
use Listboxes ItemTemplate.
something like this.
<ListBox>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FirstName}"/>
</ListBox.ItemTemplate>
</DataTemplate>
</ListBox>
In addition to the method of binding specified by the other response, you could simply bind it as follows:
listBox.ItemsSource = e.Result.Persons.Select(d => new { FirstName });
Or use the dedicated "DisplayMemberPath" property, which do exactly what you want easily without any side effects (nor additional markup):
<ListBox DisplayMemberPath="FirstName" />
For more complicated item representations, use templates (see below).
You can override the ToString() method of the Persons object so that it display the first name of the person.

Databinding between XML file and GUI

I've got a problem with my little app here that is conceptually very simple. I have an XML file that essentially just maps strings to strings.
Long-winded explanation warning
A sample file would look something like this:
<Candies>
<Sees>Box1</Sees>
<Hersheys>Box2</Hersheys>
<Godiva>Box3</Godiva>
</Candies>
Although I could use a different schema, like:
<Candies>
<Candy>
<Name>Sees</Name>
<Location>Box1</Location>
</Candy>
</Candies>
...I opted not to, since the former didn't have any forseeable adverse side effects.
In code behind, I load the contents of my XML file into an XDocument with LINQ. I also have a List variable defined, because this is what I'm databinding my GUI to. CandyLocation looks like this:
public class CandyLocation
{
public string Brand { get; set; }
public string Location { get; set; }
}
And my simple GUI is just this:
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="Auto" Width="Auto">
<Page.Resources>
<DataTemplate x:Key="CandyTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding Brand}" Margin="3"></TextBox>
<ComboBox Grid.Column="1" SelectedValue="{Binding Location}" ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Page}}, Path=DataContext.LocationNames}" Text="{Binding Location, Mode=TwoWay}" Margin="3"></ComboBox>
</Grid>
</DataTemplate>
</Page.Resources>
<DockPanel>
<Button DockPanel.Dock="Bottom" Margin="3" Command="{Binding SaveCandiesCommand}">Apply Changes</Button>
<Button DockPanel.Dock="Bottom" Margin="3" Command="{Binding AddNewCandyCommand}">Add Candy</Button>
<ListBox DockPanel.Dock="Top" ItemsSource="{Binding CandyLocations}" ItemTemplate="{StaticResource CandyTemplate}" />
</DockPanel>
</Page>
So the overview is this:
The application loads and then uses LINQ to load the XML file. When the GUI is presented, it calls "GetCandyLocations", which traverses the XML data and populates the List object with the contents of the XML file. Upon initial loading of the XML, the GUI renders properly (i.e. the candy brands and their locations appear correctly), but that's where the success story ends.
If I start from a blank XML file and add a brand, I do so by adding a new XElement to my XDocument root. Then I call OnPropertyChanged( "CandyLocations") to make the GUI update. The initial value for Location is "", so it's up to the user to select a valid location from the combobox. The problem is, I can't figure out how to get their selection databound correctly, such that I can update the XElement value. Because of this, when I save the candy locations, everything ends up with a blank location value. In addition, anytime the user clicks Add Candy, all of the previously selected location comboboxes get blanked out.
In summary:
How should I handle the selection change in the GUI? I am using MVVM for this application, so I have avoided using the ComboBox's SelectionChanged event.
Is there a way to databind directly from the GUI to the XDocument? I haven't tried it yet, but it would be best to avoid having multiple sources of data (i.e. XDocument for serialization and List for GUI rendering). Perhaps I can have the getter return the result of a LINQ query and pair it with a value converter???
How would you change my implementation if you were to write this application? I'm still learning MVVM and WPF, so any advice would be really great.
Thanks!
On your ComboBox, it looks like you might be getting a conflict between the SelectedValue and Text properties. Text is usually only used with IsEditable="True". Try using just SelectedItem:
<ComboBox SelectedItem="{Binding Location}" ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Page}}, Path=DataContext.LocationNames}" ></ComboBox>
If you want to use the XDocument directly as your data source you can use this (assuming XDocument is exposed from the VM as AvailableLocations):
<ComboBox ItemsSource="{Binding Path=AvailableLocations.Root.Elements}" SelectedValue="{Binding Location}"
SelectedValuePath="Value" DockPanel.Dock="Top" DisplayMemberPath="Value"/>
If you'd rather do something like display the company names, just change DisplayMemberPath to "Name".
Also try using an ObservableCollection instead of a List for CandyLocations so you can get automatic change notifications when items are added or removed.

Resources