I’m having the following issue with WPF ComboBox:
XAML:
<Window.Resources>
<ResourceDictionary>
<DataTemplate DataType="{x:Type this:Data}">
<ComboBox IsTextSearchEnabled="False" IsEditable="True"
Text="{Binding Value}" ItemsSource="{Binding Menu}"/>
</DataTemplate>
</ResourceDictionary>
</Window.Resources>
<StackPanel>
<ContentControl Content="{Binding}"/>
<Button Click="ChangeData_Click">Change Data</Button>
</StackPanel>
Code behind:
public Window1()
{
InitializeComponent();
DataContext = new Data();
}
void ChangeData_Click(object sender, RoutedEventArgs e)
{
DataContext = new Data();
}
I open the window and get ComboBox, bounded to my data model, I select some item (e.g. 1), all is dandy.
I change the data context to a new data model – the selected item is (to my surprise) 1... Where I don't expect any selected item...
I suspect it has something to do with the combo box which search disabled and editable, but I’m not sure what was the problem.
I found a work around: call UpdateLayout() on the ContentControl bounded to the DataContext, but it’s ugly.
Is that WPF bug? Is it all my fault?
Please Help
I've submitted the same question to MSDN WPF Forum and it seems like a Microsoft bug.
There's a workaround I found, ugly, but it's working. Here's the modified code behind:
public Window1()
{
InitializeComponent();
DataContext = new Data();
DataContextChanged += delegate { contentControl.UpdateLayout(); };
}
void ChangeData_Click(object sender, RoutedEventArgs e)
{
DataContext = null;
DataContext = new Data();
}
Note that both setting the DataContext to null and calling UpdateLayout() on DataContextChanged are needed to solve this issue.
Related
I am trying to bind a listbox via ItemsTemplate to a collection of custom "Document" objects but am having an issue while trying to bind an image to the Document.ImageResourcePath property. Here is my markup
<ListBox Name="lbDocuments">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image Source="{Binding Path=ImageResourcePath}"
Margin="5,0,5,0"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This is my load event for the form that has the listbox.
private void Window_Loaded_1(object sender, RoutedEventArgs e)
{
List<Objects.Document> docs = Objects.Document.FetchDocuments();
lbDocuments.ItemsSource = docs;
}
My Document class holds a string to a resource image located in my resources folder depending on the document extension.
e.g. (this is part of a case statement within the document class)
case Cache.DocumentType.Pdf:
this.ImageResourcePath = "/JuvenileOrganizationEmail;component/Resources/pdf_icon.jpg";
break;
When the Window loads I get absolutely nothing in my listbox when it is bound to 23 perfectly well Document types. What could I be doing wrong?
Use an ObservableCollection instead of a List, and make the reference "class level" to your Window.
ObservableCollection<Objects.Document> _docs;
Make sure the DataContext is set in the Window's Ctor.
public Window()
{
_docs = new ObservableCollection<Objects.Document>(Objects.Document.FetchDocuments());
this.DataContext = this;
}
Then, you can just update your Window Loaded event:
private void Window_Loaded_1(object sender, RoutedEventArgs e)
{
lbDocuments.ItemsSource = _docs;
}
Or, an alternative solution, will be binding the ItemsSource of the ListBox directly to a public property of the collection. This is assuming the Ctor (above) is still used.
<ListBox Name="lbDocuments" ItemsSource={Binding Docs}>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image Source="{Binding Path=ImageResourcePath}" Margin="5,0,5,0"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
In your Window.cpp file (though, a separate ViewModel class may be recommended if you are doing MVVM)
public ObservableCollection<Objects.Document> Docs
{
get { return _docs; }
}
Having application properties mapped like this:
<Application.Resources>
<properties:Settings x:Key="Settings" />
</Application.Resources>
The goal is to bind font size setting MainWindowFontSize (int) to a selected value on combobox:
<ComboBox
SelectedValuePath="Content"
SelectedValue="{Binding Default.MainWindowFontSize, Source={StaticResource Settings}}">
<ComboBoxItem>8</ComboBoxItem>
...
<ComboBoxItem>48</ComboBoxItem>
</ComboBox>
The issue with this is that it works only in one direction, from the setting into ComboBox, but any selection in the combo is not going back to the setting.
Everything seems to work fine when I use a regular property for font size in a model...
Any suggestions on how to make the binding to work with a setting both ways?
It looks to be something new in .NET 4.5. I have found though that if you create binding in the code behind it works just fine. Like so:
public MainWindow()
{
InitializeComponent();
var binding = new Binding("Delay");
binding.Source = Settings.Default;
binding.Mode = BindingMode.TwoWay;
BindingOperations.SetBinding(this.Combo, ComboBox.SelectedValueProperty, binding);
}
Have you tried setting your binding's Mode to TwoWay?
<ComboBox
SelectedValuePath="Content"
SelectedValue="{Binding Default.MainWindowFontSize, Source={StaticResource Settings}, Mode=TwoWay}">
You can try the UpdateSourceTrigger, also:
<ComboBox
SelectedValuePath="Content"
SelectedValue="{Binding Default.MainWindowFontSize, Source={StaticResource Settings}, Mode=TwoWay}, UpdateSourceTrigger=PropertyChanged">
Found this workaround:
<ComboBox ... SelectionChanged="MainWndFontSizeSelectionChanged" ...>
The event handler:
private void MainWndFontSizeSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var cb = (ComboBox)sender;
int newSize = 0;
if (Int32.TryParse(cb.SelectedValue.ToString(), out newSize) == true)
{
WpfApplication1.Properties.Settings.Default.MainWindowFontSize = newSize;
}
}
Ugly, but works... Hoping for a better solution to come up...
This post provides more insight into the issue as it appears:LINK
It does not work the same way in .NET4.5 as in the previous versions.
I used the following post to implement a datagrid bound to a list of dynamic objects
Binding DynamicObject to a DataGrid with automatic column generation?
The ITypedList method GetItemProperties works fine, a grid is displayed with all the columns I described.
I use a custom PropertyDescriptor and override the GetValue and SetValue methods as described in the above post, I also implement the TryGetMember and TrySetMember methods in the dynamic objects.
so basically I have a ComplexObject:DynamicCobject with a field Dictionary and a ComplexObjectCollection implementing ITypedList and IList.
This all works fine except when I bind the itemsSource of the DataGrid to the collection, the cells will show the SimpleObject type name and I actually want to implement a template to show the property Value of the SimpleObject in a text block.
I've used all sorts of methods to try and get the underlying SimpleObject but nothing works and I always get the ComplexObject for the row. I am using autogenerated columns and this always seems to produce a text column, this may be the problem but why cant I still get the underlying SimpleObject from somewhere in the cell properties?
Below would be my ideal solution but this does not work.
<Grid>
<Grid.Resources>
<DataTemplate x:Key="DefaultNodeTempate">
<ContentControl Content="{Binding RelativeSource={RelativeSource TemplatedParent},
Path=Content}">
<ContentControl.Resources>
<DataTemplate DataType="local:SimpleObjectType">
<TextBlock Text="{Binding Value}" />
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</DataTemplate>
</Grid.Resources>
<DataGrid ItemsSource="{Binding ElementName=mainWin, Path=DynamicObjects}">
<DataGrid.Resources>
<Style TargetType="DataGridCell">
<Setter Property="ContentTemplate" Value="{StaticResource DefaultNodeTempate}" />
</Style>
</DataGrid.Resources>
</DataGrid>
</Grid>
Any suggestions would be much appreciated.
Thanks
Kieran
So I found the solution was to do some work in the code behind.
In the AutoGeneratingColumn event create a DataTemplate with a content control and a custom template selector (I create the selector in Xaml and found it as a resource).
Create a binding for the ContentProperty of the ContentControl with e.PropertyName as the path
Create a new DataGridTemplateColumn and set the new columns CellTemplate to your new DataTemplate
replace e.Column with your new column and hey presto the cells datacontext bind with the dynamic property for that column.
If anyone has any refinement to this please feel free to share your thoughts.
Thanks
EDIT: As requested some sample code for my solution
Custom template selector:
public class CustomDataTemplateSelector : DataTemplateSelector
{
public List<DataTemplate> Templates { get; set; }
public CustomDataTemplateSelector()
: base()
{
this.Templates = new List<DataTemplate>();
}
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
DataTemplate template = null;
if (item != null)
{
template = this.Templates.FirstOrDefault(t => t.DataType is Type ? (t.DataType as Type) == item.GetType() : t.DataType.ToString() == item.GetType().ToString());
}
if (template == null)
{
template = base.SelectTemplate(item, container);
}
return template;
}
}
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Grid x:Name="ParentControl">
<Grid.Resources>
<local:CustomDataTemplateSelector x:Key="MyTemplateSelector" >
<local:CustomDataTemplateSelector.Templates>
<DataTemplate DataType="{x:Type local:MyCellObject}" >
<TextBox Text="{Binding MyStringValue}" IsReadOnly="{Binding IsReadOnly}" />
</DataTemplate>
</local:CustomDataTemplateSelector.Templates>
</local:CustomDataTemplateSelector>
</Grid.Resources>
<DataGrid ItemsSource="{Binding Rows}" AutoGenerateColumns="True" AutoGeneratingColumn="DataGrid_AutoGeneratingColumn" >
</DataGrid>
</Grid>
</Window>
Code behind:
private void DataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
// Get template selector
CustomDataTemplateSelector selector = ParentControl.FindResource("MyTemplateSelector") as CustomDataTemplateSelector;
// Create wrapping content control
FrameworkElementFactory view = new FrameworkElementFactory(typeof(ContentControl));
// set template selector
view.SetValue(ContentControl.ContentTemplateSelectorProperty, selector);
// bind to the property name
view.SetBinding(ContentControl.ContentProperty, new Binding(e.PropertyName));
// create the datatemplate
DataTemplate template = new DataTemplate { VisualTree = view };
// create the new column
DataGridTemplateColumn newColumn = new DataGridTemplateColumn { CellTemplate = template };
// set the columns and hey presto we have bound data
e.Column = newColumn;
}
There may be a better way to create the data template, I have read recently that Microsoft suggest using a XamlReader but this is how I did it back then. Also I haven't tested this on a dynamic class but I'm sure it should work either way.
I have a view model to manage a dialog type of view that allows filtering of a listing (if necessary) and selection of an item. The code works fine whether I set IsSynchronizedWithCurrentItem to true or not. My understanding is that this property is not true by default in a ListView, so I'd like to better understand this property.
Here is the binding setup in the view's xaml (which works just as well without the synch property setting):
<ListView
ItemsSource="{Binding Projects.View}"
IsSynchronizedWithCurrentItem="True"
SelectedItem="{Binding SelectedProject, Mode=TwoWay}"
>
The view model Projects is actually a CollectionViewSource that is backed by a private ObservableCollection. I think I glommed this idea from a sample project of Josh Smith's, but I honestly don't recall right now. Here is the relevant portion of the VM that relates to the xaml binding:
private ObservableCollection<ProjectViewModel> _projectsInternal { get; set; }
public CollectionViewSource Projects { get; set; }
private void _populateProjectListings(IEnumerable<Project> openProjects) {
var listing = (from p in openProjects
orderby p.Code.ToString()
select new ProjectViewModel(p)).ToList();
foreach (var pvm in listing)
pvm.PropertyChanged += _onProjectViewModelPropertyChanged;
_projectsInternal = new ObservableCollection<ProjectViewModel>(listing);
Projects = new CollectionViewSource {Source = _projectsInternal};
}
/// <summary>Property that is updated via the binding to the view</summary>
public ProjectViewModel SelectedProject { get; set; }
The Filter property of the CollectionViewSource has a handler which returns various predicates on the view model items in the list which is picked up by the bindings correctly. Here is the gist of that code (which is in the same ProjectSelctionViewModel):
/// <summary>Trigger filtering of the <see cref="Projects"/> listing.</summary>
public void Filter(bool applyFilter)
{
if (applyFilter)
Projects.Filter += _onFilter;
else
Projects.Filter -= _onFilter;
OnPropertyChanged<ProjectSelectionViewModel>(vm=>vm.Status);
}
private void _onFilter(object sender, FilterEventArgs e)
{
var project = e.Item as ProjectViewModel;
if (project == null) return;
if (!project.IsMatch_Description(DescriptionText)) e.Accepted = false;
if (!project.IsMatch_SequenceNumber(SequenceNumberText)) e.Accepted = false;
if (!project.IsMatch_Prefix(PrefixText)) e.Accepted = false;
if (!project.IsMatch_Midfix(MidfixText)) e.Accepted = false;
if (!project.IsAvailable) e.Accepted = false;
}
Setting the Mode=TwoWay is redundant since the ListView's SelectedItem binding is two way by default, but I don't mind being explicit about it (I might feel differently about that once I understand WPF better).
What about my code is making the IsSynchronizedWithCurrentItem=True redundant?
My gut is that this is decent code, but I don't like that pieces of it seem to be working via "magic", which means I would welcome any constructive feedback!
Cheers,
Berryl
IsSynchronizedWithCurrentItem syncs the CurrentItem of the default CollectionView of the bound collection with the SelectedItem of your control.
Since you never use the CurrentItem of the CollectionView and you do not appear to bind to the same collection twice, setting the property in question has no visible effect at all.
Demo of how the property syncs (for XAML viewers like XAMLPad):
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Page.Resources>
<x:Array x:Key="Items" Type="{x:Type sys:String}">
<sys:String>Apple</sys:String>
<sys:String>Orange</sys:String>
<sys:String>Pear</sys:String>
<sys:String>Lime</sys:String>
</x:Array>
</Page.Resources>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<StackPanel Background="Transparent">
<ListBox IsSynchronizedWithCurrentItem="True" ItemsSource="{StaticResource Items}" />
<ListBox IsSynchronizedWithCurrentItem="True" ItemsSource="{StaticResource Items}" />
<!-- This TextBlock binds to the CurrentItem of the Items via the "/" -->
<TextBlock Text="{Binding Source={StaticResource Items}, Path=/}"/>
</StackPanel>
</ScrollViewer>
</Page>
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.