OxyPlot WPF not working with Button Click - wpf

I’m having some problems with OxyPlot that I have not been able to resolve through their documentation or other searches. I’m working on a wpf application that will allow the user to open a .csv with a button-click event, then perform some math and report back some useful information. I’d like to plot some of the generated data hence OxyPlot. For some reason I cannot get the plot to populate, when the code that generates it, is within the button click event. To illustrate here is a smaller example:
This code works (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:oxy="http://oxyplot.org/wpf"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button x:Name="button" Content="Button" HorizontalAlignment="Left" Margin="20,20,0,0" VerticalAlignment="Top" Width="75" Click="button_Click"/>
<Grid HorizontalAlignment="Left" Height="255" Margin="20,47,0,0" VerticalAlignment="Top" Width="477">
<oxy:PlotView Model="{Binding ScatterModel}"/>
</Grid>
</Grid>
with this:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
var tmp = new PlotModel { Title = "Scatter plot", Subtitle = "y = x" };
var s2 = new LineSeries
{
StrokeThickness = 1,
MarkerSize = 1,
MarkerStroke = OxyColors.ForestGreen,
MarkerType = MarkerType.Plus
};
for (int i = 0; i < 100; i++)
{
s2.Points.Add(new DataPoint(i, i));
}
tmp.Series.Add(s2);
this.ScatterModel = tmp;
}
private void button_Click(object sender, RoutedEventArgs e)
{
}
public PlotModel ScatterModel { get; set; }
And produces this:
Plot Working
But, without changing the xaml, if I copy/paste the code beneath the button click event:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void button_Click(object sender, RoutedEventArgs e)
{
DataContext = this;
var tmp = new PlotModel { Title = "Scatter plot", Subtitle = "y = x" };
var s2 = new LineSeries
{
StrokeThickness = 1,
MarkerSize = 1,
MarkerStroke = OxyColors.ForestGreen,
MarkerType = MarkerType.Plus
};
for (int i = 0; i < 100; i++)
{
s2.Points.Add(new DataPoint(i, i));
}
tmp.Series.Add(s2);
this.ScatterModel = tmp;
}
public PlotModel ScatterModel { get; set; }
The plot never generates: Not working:
I’ve tried moving DataContext = this; back up to public MainWindow(), and vice-versa with InitializeComponent(); no change. I’ve also tried defining
<Window.DataContext>
<local:MainWindow/>
</Window.DataContext>
in the xaml but that throws an exception/infinite loop error during build.
Something simple I fear I'm not getting about OxyPlot implementation?
Thanks!
CSMDakota

INotifyPropertyChanged keeps your view in sync with the program's state. One way to do this is by implementing a ViewModel (the MVVM pattern).
So let's create one. ViewModelBase introduces OnPropertyChanged(), the method that updates ScatterModel.
ViewModels.cs
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using OxyPlot;
namespace WpfApplication1
{
public class ViewModel : ViewModelBase
{
private PlotModel _scatterModel;
public PlotModel ScatterModel
{
get { return _scatterModel; }
set
{
if (value != _scatterModel)
{
_scatterModel = value;
OnPropertyChanged();
}
}
}
}
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] String propName = null)
{
// C#6.O
// PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
if (PropertyChanged != null)
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
}
In MainWindow.xaml you can now add
<Window.DataContext>
<local:ViewModel/>
</Window.DataContext>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void button_Click(object sender, RoutedEventArgs e)
{
var tmp = new PlotModel { Title = "Scatter plot", Subtitle = "y = x" };
var s2 = new LineSeries
{
StrokeThickness = 1,
MarkerSize = 1,
MarkerStroke = OxyColors.ForestGreen,
MarkerType = MarkerType.Plus
};
for (int i = 0; i < 100; i++)
{
s2.Points.Add(new DataPoint(i, i));
}
tmp.Series.Add(s2);
ViewModel.ScatterModel = tmp;
}
// C#6.O
// public ViewModel ViewModel => (ViewModel)DataContext;
public ViewModel ViewModel
{
get { return (ViewModel)DataContext; }
}
}
Note we're no longer setting DataContext = this, which is considered bad practice. In this case the ViewModel is small, but as a program grows this way of structuring pays off.

Related

wpf binding synchronization across different controls in separate windows

I have three windows in whose i need a single value synchronized.
I need to update the selected item across thre windows. Two of them containing a combobox and the main window containing a label. Thus, i need the two comboboxes to be identical and the label do update reflecting the selected item in the comboboxes.
Curently I get the two combos syncronized but my question is how to sync the label. I´ve tried using Properties, using DependencyProperties, Implementing INotifyPropertyChanged but no luck with the label
i have this example code failing to synchronize the label:
MainWindow.cs:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public CollectionView Items { get; set; }
private string _selectedItem;
public event PropertyChangedEventHandler PropertyChanged;
public string SelectedItem
{
get { return (string)GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem",
typeof(string), typeof(MainWindow), new UIPropertyMetadata(""));
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public MainWindow()
{
DataContext = this;
InitializeComponent();
Left = 100; Width = 350; Height = 200; Top = 10;
Business business = new Business();
Items = new CollectionView(business.GetItemsFromWebService());
Wnd1 wnd1 = new Wnd1();
wnd1.Left = 100; wnd1.Width = 350; wnd1.Height = 200; wnd1.Top = 210;
wnd1.Show();
Wnd2 wnd2 = new Wnd2();
wnd2.Left = 100; wnd2.Width = 350; wnd2.Height = 200; wnd2.Top = 410;
wnd2.Show();
wnd1.cbItems.ItemsSource = Items;
wnd1.cbItems.SelectedValue = SelectedItem;
wnd2.cbItems.ItemsSource = Items;
wnd2.cbItems.SelectedValue = SelectedItem;
Binding labelBinding = new Binding();
labelBinding.Mode = BindingMode.TwoWay;
labelBinding.Source = SelectedItem;
labelBinding.Path = new PropertyPath("SelectedItem");
lbSelected.SetBinding(Label.ContentProperty, labelBinding);
Binding cmbBindingW1 = new Binding();
cmbBindingW1.Mode = BindingMode.TwoWay;
cmbBindingW1.Source = SelectedItem;
cmbBindingW1.Path = new PropertyPath("SelectedItem");
wnd1.cbItems.SetBinding(ComboBox.SelectedItemProperty, cmbBindingW1);
wnd1.cbItems.SelectionChanged += CbItems_SelectionChanged;
Binding cmbBindingW2 = new Binding();
cmbBindingW2.Mode = BindingMode.TwoWay;
cmbBindingW2.Source = SelectedItem;
cmbBindingW2.Path = new PropertyPath("SelectedItem");
wnd1.cbItems.SetBinding(ComboBox.SelectedItemProperty, cmbBindingW2);
}
private void CbItems_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
int a = 0;
SelectedItem = ((ComboBox)sender).SelectedItem.ToString();
}
}
MainWindow.xaml:
<Window x:Class="DropDownSync.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DropDownSync"
mc:Ignorable="d"
Title="MainWindow" Height="183.333" Width="376.667"
Name="wMain">
<Grid>
<Label Name="lbSelected" HorizontalAlignment="Left" Margin="10,71,0,0" VerticalAlignment="Top" Width="80"
Visibility="Visible"/>
</Grid>
</Window>
Business.cs:
public class Business
{
public List<string> GetItemsFromWebService()
{
List<string> result = new List<string>();
result.Add("item1");
result.Add("item2");
result.Add("item3");
return result;
}
}
Wnd1.xaml:
<Window x:Class="DropDownSync.Wnd1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DropDownSync"
mc:Ignorable="d"
Title="Wnd1" Height="211.667" Width="518.333"
Name="w1">
<Grid>
<ComboBox Name="cbItems" HorizontalAlignment="Left" Margin="90,14,0,0" VerticalAlignment="Top" Width="120"/>
<Label Content="Current item" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="80"/>
</Grid>
</Window>
Wnd2.xaml:
<Window x:Class="DropDownSync.Wnd2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DropDownSync"
mc:Ignorable="d"
Title="Wnd2" Height="190" Width="453.333"
Name="w2">
<Grid>
<ComboBox Name="cbItems" HorizontalAlignment="Left" Margin="90,14,0,0" VerticalAlignment="Top" Width="120"/>
<Label Content="Current item" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="80"/>
</Grid>
</Window>
new CollectionView() causes Visual Studio 2017 to output this in debug output:
System.Windows.Data Warning: 53 : Using CollectionView directly is not
fully supported. The basic features work, although with some
inefficiencies, but advanced features may encounter known bugs.
Consider using a derived class to avoid these problems.
Instead, I'd change it to type ICollectionView and then set Items to CollectionViewSource.GetDefaultView(business.GetItemsFromWebService());
Also, you set wnd1.cbItems.Binding(ComboBox.SelectedItemProperty, xxx) twice. I assume the 2nd one was supposed to be wnd2.
And the SelectionChanged event handler isn't needed, if bindings are correct.
Here is an updated MainWindow.cs with the changes:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public ICollectionView Items { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public string SelectedItem
{
get { return (string)GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem",
typeof(string), typeof(MainWindow), new UIPropertyMetadata(""));
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public MainWindow()
{
DataContext = this;
InitializeComponent();
Left = 100; Width = 350; Height = 200; Top = 10;
Business business = new Business();
Items = CollectionViewSource.GetDefaultView(business.GetItemsFromWebService());
Wnd1 wnd1 = new Wnd1();
wnd1.Left = 100; wnd1.Width = 350; wnd1.Height = 200; wnd1.Top = 210;
wnd1.Show();
Wnd2 wnd2 = new Wnd2();
wnd2.Left = 100; wnd2.Width = 350; wnd2.Height = 200; wnd2.Top = 410;
wnd2.Show();
wnd1.cbItems.ItemsSource = Items;
wnd2.cbItems.ItemsSource = Items;
Binding labelBinding = new Binding();
labelBinding.Mode = BindingMode.TwoWay;
labelBinding.Source = this;
labelBinding.Path = new PropertyPath("SelectedItem");
// no need to make an identical bindings, just use the same one again
lbSelected.SetBinding(Label.ContentProperty, labelBinding);
wnd1.cbItems.SetBinding(ComboBox.SelectedItemProperty, labelBinding);
wnd2.cbItems.SetBinding(ComboBox.SelectedItemProperty, labelBinding);
}
}

Binding is not working with dynamic data updates

I'm new in WPF and I have the following problem.
I have the following class with many properties , but here is only one property for example:
public class StatusData : INotifyPropertyChanged
{
private string m_statusText = String.Empty;
public StatusData()
{
m_statusText = "1234";
}
public string StatusText
{
get
{
return m_statusText;
}
set
{
if (m_statusText != value)
{
m_statusText = value;
NotifyPropertyChanged("StatusText");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Another component of the project changes StatusData and calls Update() function in MainWindow.
So, m_statusData of this MainWindow has changed and I want update the textbox with m_statusText accordingly.
public class MainWindow
{
private StatusData m_statusData = new StatusData();
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
grid1.DataContext = m_statusData ;
}
public void Update(StatusData newStatusData)
{
m_statusData = newStatusData;
}
}
Xaml code:
<Window x:Class="WpfApplicationUpdateTextBox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="myWin"
xmlns:local="clr-namespace:WpfApplicationUpdateTextBox"
Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded" >
<Grid Name="grid1">
<TextBox Text="{Binding Path=StatusText}" Name="textBox1" />
</Grid>
</Window>
The question is : why the textBox is not updated withnewStatusData.StatusText?
Here, you are assigning the grid's DataContext to m_statusData:
grid1.DataContext = m_statusData ;
And here, you are reassigning m_statusData to something else:
m_statusData = newStatusData;
The problem is that this has no effect on grid1.DataContext, which was set to the previous instance of m_statusData.
In this case, doing grid1.DataContext = newStatusData should solve your problem. However, a better solution would be to create a StatusData property which returns m_statusData. You can then do a RaisePropertyChanged() on it when m_statusData changes.
private void Update(StatusData newStatusData)
{
StatusData = newStatusData;
}
public StatusData StatusData
{
get
{
return m_statusData;
}
set
{
m_statusData = value;
RaisePropertyChanged("StatusData");
}
}
... and then in your XAML, bind your Grid's DataContext to the StatusData property
Edit:
To bind the grid's data context to the StatusData property, you can do this in your XAML:
<Grid Name="grid1" DataContext="{Binding StatusData}">
<TextBox Text="{Binding Path=StatusText}" Name="textBox1" />
</Grid>
You will also need to set the initial DataContext of your window, to make all of the other databindings work (this is a little strange and non-standard, but it will do the trick):
this.DataContext = this;
Your class StatusData has only 1 property: StatusText. If this property is the only thing you want to change with this code:
m_statusData = newStatusData;
You can change it to this:
m_statusData.StatusText = newStatusData.StatusText;
This code will fire the PropertyChangedEvent of StatusData class and that will change TextBox value.

Mutually Exclusive comboboxes that binds to same data source - MVVM implementation

I'm not sure my Title is right but this is the problem I am facing now.. I have the below XAML code..
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<ComboBox ItemsSource="{Binding Path=AvailableFields}"
SelectedItem="{Binding Path=SelectedField}"
></ComboBox>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
What this basically does is, If my data source contains ten items, this is going to generate 10 row of comboboxes and all comboboxes are bounded to the same itemsource.
Now my requirement is Once an item is selected in the first combo box, that item should not be available in the subsequent combo boxes. How to satisfy this requirement in MVVM and WPF?
This turned out to be harder than I thought when I started coding it. Below sample does what you want. The comboboxes will contain all letters that are still available and not selected in another combobox.
XAML:
<Window x:Class="TestApp.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<StackPanel>
<ItemsControl ItemsSource="{Binding Path=SelectedLetters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ComboBox
ItemsSource="{Binding Path=AvailableLetters}"
SelectedItem="{Binding Path=Letter}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Window>
Code behind:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
namespace TestApp
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new VM();
}
}
public class VM : INotifyPropertyChanged
{
public VM()
{
SelectedLetters = new List<LetterItem>();
for (int i = 0; i < 10; i++)
{
LetterItem letterItem = new LetterItem();
letterItem.PropertyChanged += OnLetterItemPropertyChanged;
SelectedLetters.Add(letterItem);
}
}
public List<LetterItem> SelectedLetters { get; private set; }
private void OnLetterItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != "Letter")
{
return;
}
foreach (LetterItem letterItem in SelectedLetters)
{
letterItem.RefreshAvailableLetters(SelectedLetters);
}
}
public event PropertyChangedEventHandler PropertyChanged;
public class LetterItem : INotifyPropertyChanged
{
static LetterItem()
{
_allLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".Select(c => c.ToString());
}
public LetterItem()
{
AvailableLetters = _allLetters;
}
public void RefreshAvailableLetters(IEnumerable<LetterItem> letterItems)
{
AvailableLetters = _allLetters.Where(c => !letterItems.Any(li => li.Letter == c) || c == Letter);
}
private IEnumerable<string> _availableLetters;
public IEnumerable<string> AvailableLetters
{
get { return _availableLetters; }
private set
{
_availableLetters = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("AvailableLetters"));
}
}
}
private string _letter;
public string Letter
{
get { return _letter; }
set
{
if (_letter == value)
{
return;
}
_letter = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Letter"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private static readonly IEnumerable<string> _allLetters;
}
}
}
This functionality is not provided by WPF, but it can be implemented using some custom coding.
I've created 3 ViewModel classes:
PreferencesVM - This will be our DataContext. It contains the master list of options which can appear in the ComboBoxes, and also contains a SelectedOptions property, which keeps track of which items are selected in the various ComboBoxes. It also has a Preferences property, which we will bind our ItemsControl.ItemsSource to.
PreferenceVM - This represents one ComboBox. It has a SelectedOption property, which ComboBox.SelectedItem is bound to. It also has a reference to PreferencesVM, and a property named Options (ComboBox.ItemsSource is bound to this), which returns the Options on PreferencesVM via a filter which checks if the item may be displayed in the ComboBox.
OptionVM - Represents a row in the ComboBox.
The following points form the key to the solution:
When PreferenceVM.SelectedOption is set (ie a ComboBoxItem is selected), the item is added to the PreferencesVM.AllOptions collection.
PreferenceVM handles Preferences.SelectedItems.CollectionChanged, and triggers a refresh by raising PropertyChanged for the Options property.
PreferenceVM.Options uses a filter to decide which items to return - which only allows items which are not in PreferencesVM.SelectedOptions, unless they are the SelectedOption.
What I've described above might be enough to get you going, but to save you the headache I'll post my code below.
PreferencesVM.cs:
public class PreferencesVM
{
public PreferencesVM()
{
PreferenceVM pref1 = new PreferenceVM(this);
PreferenceVM pref2 = new PreferenceVM(this);
PreferenceVM pref3 = new PreferenceVM(this);
this._preferences.Add(pref1);
this._preferences.Add(pref2);
this._preferences.Add(pref3);
//Only three ComboBoxes, but you can add more here.
OptionVM optRed = new OptionVM("Red");
OptionVM optGreen = new OptionVM("Green");
OptionVM optBlue = new OptionVM("Blue");
_allOptions.Add(optRed);
_allOptions.Add(optGreen);
_allOptions.Add(optBlue);
}
private ObservableCollection<OptionVM> _selectedOptions =new ObservableCollection<OptionVM>();
public ObservableCollection<OptionVM> SelectedOptions
{
get { return _selectedOptions; }
}
private ObservableCollection<OptionVM> _allOptions = new ObservableCollection<OptionVM>();
public ObservableCollection<OptionVM> AllOptions
{
get { return _allOptions; }
}
private ObservableCollection<PreferenceVM> _preferences = new ObservableCollection<PreferenceVM>();
public ObservableCollection<PreferenceVM> Preferences
{
get { return _preferences; }
}
}
PreferenceVM.cs:
public class PreferenceVM:INotifyPropertyChanged
{
private PreferencesVM _preferencesVM;
public PreferenceVM(PreferencesVM preferencesVM)
{
_preferencesVM = preferencesVM;
_preferencesVM.SelectedOptions.CollectionChanged += new NotifyCollectionChangedEventHandler(SelectedOptions_CollectionChanged);
}
void SelectedOptions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this,new PropertyChangedEventArgs("Options"));
}
private OptionVM _selectedOption;
public OptionVM SelectedOption
{
get { return _selectedOption; }
set
{
if (value == _selectedOption)
return;
if (_selectedOption != null)
_preferencesVM.SelectedOptions.Remove(_selectedOption);
_selectedOption = value;
if (_selectedOption != null)
_preferencesVM.SelectedOptions.Add(_selectedOption);
}
}
private ObservableCollection<OptionVM> _options = new ObservableCollection<OptionVM>();
public IEnumerable<OptionVM> Options
{
get { return _preferencesVM.AllOptions.Where(x=>Filter(x)); }
}
private bool Filter(OptionVM optVM)
{
if(optVM==_selectedOption)
return true;
if(_preferencesVM.SelectedOptions.Contains(optVM))
return false;
return true;
}
public event PropertyChangedEventHandler PropertyChanged;
}
OptionVM.cs:
public class OptionVM
{
private string _name;
public string Name
{
get { return _name; }
}
public OptionVM(string name)
{
_name = name;
}
}
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new PreferencesVM();
}
}
MainWindow.xaml:
<Window x:Class="WpfApplication64.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ItemsControl ItemsSource="{Binding Path=Preferences}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Path=Options}" DisplayMemberPath="Name" SelectedItem="{Binding Path=SelectedOption}"></ComboBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
**Note that to reduce lines of code, my provided solution only generates 3 ComboBoxes (not 10).

What is causing my WPF combobox items to take so long to refresh when the itemssource is changed?

I have a datagrid (call it dat1) that has an items source bound to an observable collection of a custom type, call it TypeA. One of the properties on TypeA is an observable collection of another custom type, call it TypeB. I then have a combobox with an items source bound to dat1's SelectedItem.TypeB.
So when the user selects a TypeA in dat1, the combobox shows the items in the TypeB observable collection from the selected TypeA. Make sense?
The binding DOES work and it DOES update. The problem is that when the items presenter in the combobox has already displayed items and the user selects a different TypeA in dat1 and tries to view the new items in the combobox, there is a long pause while the items presenter generates the new items.
To test the problem, I can simplify the scenario.
Steps to reproduce:
Create a new WPF project using .NET 4.0.
Cut and Paste the code below.
To get the freezing behavior, you must drop the combobox to see the items, then click the button so the items source changes, and then drop the combobox again. The combobox drops after a few seconds, but why so slow?
XAML
<Window x:Class="ComboBoxTest.MainWindow"
xmlns:System="clr-namespace:System;assembly=mscorlib"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel>
<ComboBox x:Name="cbo" DisplayMemberPath="Junk1"></ComboBox>
<Button Content="Click Me!" Click="btn_Click"></Button>
</StackPanel>
</Grid>
</Window>
Code
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.cbo.ItemsSource = junk1;
}
ObservableCollection<Junk> junk1 = new ObservableCollection<Junk>() {
new Junk() { Junk1 = "junk1 - 1" },
new Junk() { Junk1 = "junk1 - 2" } };
ObservableCollection<Junk> junk2 = new ObservableCollection<Junk>() {
new Junk() { Junk1 = "junk2 - 1" },
new Junk() { Junk1 = "junk2 - 2" },
new Junk() { Junk1 = "junk2 - 3" },
new Junk() { Junk1 = "junk2 - 4" } };
private void btn_Click(object sender, RoutedEventArgs e)
{
if (this.cbo.ItemsSource == junk1)
this.cbo.ItemsSource = junk2;
else
this.cbo.ItemsSource = junk1;
}
}
public class Junk
{
public string Junk1 { get; set; }
}
NOTE: This is a WPF problem. I've heard Silverlight doesn't have the same issue. I don't need to know if Silverlight works. I need a WPF answer.
PS. The delay is longer when the items source is changed to junk2, presumably because it is larger.
It delays enough that I think it may be caused by binding exceptions, since exceptions take time. Is there a way to see if there are binding exceptions being thrown?
I observe this phenomenon too.
I'm using Visual Studio 2010 (with ReSharper 6.0) on Windows 7 x64.
It's not noticeable with only four items as in the example above, but if I make it e.g. 50 or more items the freeze gets very noticeable.
After the rebinding it will then hang for about 15 seconds before I'm allowed to interact with it again.
Another interesting thing is that this only happens while debugging in VS. If I run the exe standalone it is really snappy and fast.
Here is the code from my simple project:
XAML
<Window x:Class="ComboBoxFreeze.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<ComboBox x:Name="cbo" DisplayMemberPath="Junk1"></ComboBox>
<Button Content="Click Me!" Click="btn_Click"></Button>
</StackPanel>
</Window>
Code
using System.Collections.ObjectModel;
using System.Windows;
namespace ComboBoxFreeze
{
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
_junk1 = new ObservableCollection<Junk>();
for (int i = 0; i < 50; i++)
{
_junk1.Add(new Junk { Junk1 = "Prop1a-" + i, Junk2 = "Prop1b-" + i });
}
_junk2 = new ObservableCollection<Junk>();
for (int i = 0; i < 50; i++)
{
_junk2.Add(new Junk { Junk1 = "Prop2a-" + i, Junk2 = "Prop2b-" + i });
}
}
private readonly ObservableCollection<Junk> _junk1;
private readonly ObservableCollection<Junk> _junk2;
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
cbo.ItemsSource = _junk1;
}
private void btn_Click(object sender, RoutedEventArgs e)
{
if (cbo.ItemsSource == _junk1)
{
cbo.ItemsSource = _junk2;
}
else
{
cbo.ItemsSource = _junk1;
}
}
}
public class Junk
{
public string Junk1 { get; set; }
public string Junk2 { get; set; }
}
}
I will post here again if I find a solution or workaround to this.

WPF 4 Drag and Drop with visual element as cursor

I have a WPF 4 app which I want to enable drag and drop with, currently I have it working with a basic drag and drop implementation, but I have found that it would be much better if, instead of the mouse cursor changing over to represent the move operation, I could use an image underneath my finger.
My drag and drop operation is initiated inside a custom user control, so I will need to insert a visual element into the visual tree and have it follow my finger around, perhaps I should enable the ManipulationDelta event on my main window, check for a boolean then move the item around?
From the mentioned article I was able to simplify a little. Basically what you need to do is subscribe in 3 events:
PreviewMouseLeftButtonDownEvent: The event that runs when you press the left button, you can start the drag action by invoking DragDrop.DoDragDrop
DropEvent: The event that runs when you drop something (control must have AllowDrop set to true in order to accept drops)
GiveFeedbackEvent: The event that runs all the time allowing you to give constant feedback
DragDrop.DoDragDrop(draggedItem, draggedItem.DataContext, DragDropEffects.Move); the first parameter is the element you are dragging, then the second is the data it is carrying and last the mouse effect.
This method locks the thread. So everything after its call will only execute when you stop dragging.
In the drop event you can retrieve the data you sent on the DoDragDrop call.
The source for my tests are located bellow, and the result is:
Sample drag n' drop (gif)
Full Source
MainWindow.xaml
<Window x:Class="TestWpfPure.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:uc="clr-namespace:TestWpfPure"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ListBox x:Name="CardListControl" AllowDrop="True" ItemsSource="{Binding Items}" />
</Grid>
</Window>
Card.xaml
<UserControl x:Class="TestWpfPure.Card"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Border x:Name="CardBorder" BorderBrush="Black" BorderThickness="3" HorizontalAlignment="Left" Height="40" VerticalAlignment="Top" Width="246" RenderTransformOrigin="0.5,0.5" CornerRadius="6">
<TextBlock Text="{Binding Text}" TextWrapping="Wrap" FontFamily="Arial" FontSize="14" />
</Border>
</Grid>
</UserControl>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Effects;
using System.Windows.Shapes;
namespace TestWpfPure
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ObservableCollection<Card> Items { get; set; }
private readonly Style listStyle = null;
private Window _dragdropWindow = null;
public MainWindow()
{
InitializeComponent();
Items = new ObservableCollection<Card>(new List<Card>
{
new Card { Text = "Task #01" },
new Card { Text = "Task #02" },
new Card { Text = "Task #03" },
new Card { Text = "Task #04" },
new Card { Text = "Task #05" },
});
listStyle = new Style(typeof(ListBoxItem));
listStyle.Setters.Add(new Setter(ListBoxItem.AllowDropProperty, true));
listStyle.Setters.Add(new EventSetter(ListBoxItem.PreviewMouseLeftButtonDownEvent, new MouseButtonEventHandler(CardList_PreviewMouseLeftButtonDown)));
listStyle.Setters.Add(new EventSetter(ListBoxItem.DropEvent, new DragEventHandler(CardList_Drop)));
listStyle.Setters.Add(new EventSetter(ListBoxItem.GiveFeedbackEvent, new GiveFeedbackEventHandler(CardList_GiveFeedback)));
CardListControl.ItemContainerStyle = listStyle;
DataContext = this;
}
protected void CardList_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (sender is ListBoxItem)
{
var draggedItem = sender as ListBoxItem;
var card = draggedItem.DataContext as Card;
card.Effect = new DropShadowEffect
{
Color = new Color { A = 50, R = 0, G = 0, B = 0 },
Direction = 320,
ShadowDepth = 0,
Opacity = .75,
};
card.RenderTransform = new RotateTransform(2.0, 300, 200);
draggedItem.IsSelected = true;
// create the visual feedback drag and drop item
CreateDragDropWindow(card);
DragDrop.DoDragDrop(draggedItem, draggedItem.DataContext, DragDropEffects.Move);
}
}
protected void CardList_Drop(object sender, DragEventArgs e)
{
var droppedData = e.Data.GetData(typeof(Card)) as Card;
var target = (sender as ListBoxItem).DataContext as Card;
int targetIndex = CardListControl.Items.IndexOf(target);
droppedData.Effect = null;
droppedData.RenderTransform = null;
Items.Remove(droppedData);
Items.Insert(targetIndex, droppedData);
// remove the visual feedback drag and drop item
if (this._dragdropWindow != null)
{
this._dragdropWindow.Close();
this._dragdropWindow = null;
}
}
private void CardList_GiveFeedback(object sender, GiveFeedbackEventArgs e)
{
// update the position of the visual feedback item
Win32Point w32Mouse = new Win32Point();
GetCursorPos(ref w32Mouse);
this._dragdropWindow.Left = w32Mouse.X;
this._dragdropWindow.Top = w32Mouse.Y;
}
private void CreateDragDropWindow(Visual dragElement)
{
this._dragdropWindow = new Window();
_dragdropWindow.WindowStyle = WindowStyle.None;
_dragdropWindow.AllowsTransparency = true;
_dragdropWindow.AllowDrop = false;
_dragdropWindow.Background = null;
_dragdropWindow.IsHitTestVisible = false;
_dragdropWindow.SizeToContent = SizeToContent.WidthAndHeight;
_dragdropWindow.Topmost = true;
_dragdropWindow.ShowInTaskbar = false;
Rectangle r = new Rectangle();
r.Width = ((FrameworkElement)dragElement).ActualWidth;
r.Height = ((FrameworkElement)dragElement).ActualHeight;
r.Fill = new VisualBrush(dragElement);
this._dragdropWindow.Content = r;
Win32Point w32Mouse = new Win32Point();
GetCursorPos(ref w32Mouse);
this._dragdropWindow.Left = w32Mouse.X;
this._dragdropWindow.Top = w32Mouse.Y;
this._dragdropWindow.Show();
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetCursorPos(ref Win32Point pt);
[StructLayout(LayoutKind.Sequential)]
internal struct Win32Point
{
public Int32 X;
public Int32 Y;
};
}
}
Card.xaml.cs
using System.ComponentModel;
using System.Windows.Controls;
namespace TestWpfPure
{
/// <summary>
/// Interaction logic for Card.xaml
/// </summary>
public partial class Card : UserControl, INotifyPropertyChanged
{
private string text;
public string Text
{
get
{
return this.text;
}
set
{
this.text = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Text"));
}
}
public Card()
{
InitializeComponent();
DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
There is an example of using a custom drag cursor at Jaime Rodriguez msdn blog. You can handle the GiveFeedback event and change the mouse cursor, but to use a custom Visual the author creates a new Window and updates the position on QueryContinueDrag.

Resources