WPF: Changing tabs makes Windowsformshost child disappear - wpf

if you can spare the time, I am working on a problem for which I can't find a solution on the internet.
I need two tabs' richtextboxes to bind the same property. Both RichtextBoxes are hosted in WPF via Windowsformshost. But if I alternate between tabs, one RichtTextBox will simply dissapear (always the first one that was visible). I am migrating an app and so far, I am forced to use the Windowsforms RichtextBox.
I hope I managed to properly convey my problem - sorry, I am not a native speaker.
Thanks in advance
Edit:
I was asked to provide a clear example of my problem. Thanks for the note. I completely rewrote my question. Further, I have uploaded a micro app where I have isolated the problem. Just click the two tab buttons alternately and one Richtextbox will dissapear.
Below, I will provide the code if this serves:
This is my Mainwindow (XAML):
<StackPanel Orientation="Horizontal" Height="35" Margin="0,35,0,0" VerticalAlignment="Top" >
<Button x:Name="Tab1" Command="{Binding LeftCommand}" Content="Left" MinWidth="100" />
<Button x:Name="Tab2" Command="{Binding RightCommand}" Content="Right" MinWidth="100" />
</StackPanel>
<Frame x:Name="MyFrame"
Content="{Binding Path=CurrentTab, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Margin="5,70,0,0" NavigationUIVisibility="Hidden" />
This is its viewmodel:
class MainWindowViewModel : INotifyPropertyChanged
{
public ICommand LeftCommand { get; }
public ICommand RightCommand { get; }
private TabViewModel MyTabViewModel { get; set; }
private PageLeft MyPageLeft { get; set; }
private PageRight MyPageRight { get; set; }
public MainWindowViewModel()
{
this.LeftCommand = new ModelCommand(p => this.SetSelectedTab("left"));
this.RightCommand = new ModelCommand(p => this.SetSelectedTab("right"));
this.MyTabViewModel = new TabViewModel();
this.MyPageLeft = new PageLeft() { DataContext = this.MyTabViewModel };
this.MyPageRight = new PageRight() { DataContext = this.MyTabViewModel };
//initial view on something
//this.SetSelectedTab("left");
}
private void SetSelectedTab(string param)
{
switch (param)
{
case "left":
this.CurrentTab = this.MyPageLeft;
break;
case "right":
this.CurrentTab = this.MyPageRight;
break;
}
}
private object _CurrentTab;
public object CurrentTab
{
get { return _CurrentTab; }
set
{
if (value != _CurrentTab)
{
_CurrentTab = value;
NotifyPropertyChanged_MainViewModel();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
private void NotifyPropertyChanged_MainViewModel([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Furthermore, I have two pages (MyPageLeft, MyPageRight) that use the same viewmodel (TabViewModel) and use the same bit of XAML code:
<ContentControl Content="{Binding Path=MyWindowsFormsHost, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
Both Pages use the same TabViewModel:
class TabViewModel : INotifyPropertyChanged
{
private WindowsFormsHost _MyWindowsFormsHost;
public WindowsFormsHost MyWindowsFormsHost
{
get { return _MyWindowsFormsHost; }
set
{
if (value != _MyWindowsFormsHost)
{
_MyWindowsFormsHost = value;
NotifyPropertyChanged_TabViewModel();
}
}
}
public TabViewModel()
{
this.MyWindowsFormsHost = new WindowsFormsHost() { Child = new RichTextBox() { Text = "test" } };
}
public event PropertyChangedEventHandler PropertyChanged;
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
private void NotifyPropertyChanged_TabViewModel([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
The Problem: If I start the app and click on the two tab buttons alternatingly, one of the framed RichtextBoxes will dissapear.

If anyone might need it, I used a dirty solution - although it might not be recommendable.
I extended the event of the switch tab buttons. It takes the RTF property of the currently selected Tab's Richtextbox and infuses it in the other Richtextbox. It goes kinda like this:
if (Tab2 Button is clicked)
this.MyRTF = Tab1.Richtextbox.RTF;
Tab2.Richttextbox.Rtf = this.MyRTF;
Note that this is a beginner's hack on a probably overall questionable approach.
Thanks to anyone who read my question!

Related

WPF MVVM Data Binding - One Way?

I am new to WPF but have an small understanding of MVVM, so far this is what I have implemented.
UpdateTableView - View (Short snippet of larger user control)
<UserContol.DataContext>
<local:UpdateTableViewModel />
</UserContol.DataContext>
<StackPanel>
<TextBox Text="{Binding InputPath}"/>
<TextBlock Content="Placeholder" />
</StackPanel>
UpdateTableModel - Model
public class UpdateTableModel : ObservableObject
{
private string _inputPath;
public string InputPath
{
get
{
return _inputPath;
}
set
{
if (value != _inputPath)
{
_inputPath = value;
OnPropertyChanged("InputPath");
}
}
}
}
ObservableObject
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanaged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanaged;
if (handler != null)
{
PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
}
And an empty UpdateTableViewModel
class UpdateTableViewModel : ObservableObject { }
My question is how would I use data binding so that as a user when I enter a inputPath in the text box, firstly whatever I type is store in the property _inputPath so I can use it in code behind and additionally be reflected in the text block.
I have done some research and found about one way and two way data binding and can't really work out what else I need to add for my desired functionality.
Thanks in advance.
Your view models must contain the properties you want to bind to.
Generally the TextBox.Text property automatically binds TwoWay. This is the default behavior. So, without specifying the Binding.Mode explicitly, the text entered into the TextBox will be automatically sent to the binding source. In your case the input would be automatically sent to the InputPath property.
UpdateTableModel.cs
public class UpdateTableModel
{
public void SaveUserNameToFile(string filePath, string userName)
{
File.AppendAllText(filePath, userName, Encoding.UTF8);
}
}
UpdateTableViewModel.cs
An implementation of RelayCommand can be found at
Microsoft Docs: Patterns - WPF Apps With The Model-View-ViewModel Design Pattern - Relaying Command Logic
class UpdateTableViewModel : ObservableObject
{
private UpdateTableModel UpdateTableModel { get; }
public ICommand SaveUserCommand => new RelayCommand(SaveUserName);
private string _userName;
public string UserName
{
get => _userName;
set
{
if (value != _userName)
{
_userName = value;
OnPropertyChanged(nameof(UserName));
}
}
}
private string _inputPath;
public string InputPath
{
get => _inputPath;
set
{
if (value != _inputPath)
{
_inputPath = value;
OnPropertyChanged(nameof(InputPath));
}
}
}
public UpdateTableViewModel()
{
this.UpdateTableModel = new UpdateTableModel();
}
// Alternative constructor
public UpdateTableViewModel(UpdateTableModel updateTableModel)
{
this.UpdateTableModel = updateTableModel;
}
private void SaveUserName(object param)
{
// Pass the data to the model
this.UpdateTableModel.SaveUserNameToFile(this.InputPath, this.UserName);
}
}
UpdateTableView.xaml
<UserControl>
<UserContol.DataContext>
<local:UpdateTableViewModel />
</UserContol.DataContext>
<StackPanel>
<TextBox Text="{Binding UserName}" />
<TextBox Text="{Binding InputPath}" />
<Button Command="{Binding SaveUserCommand}"
Content="Save to File" />
</StackPanel>
</UserControl>

MVVM Binding to 2 controls not working

I'm trying to make a control that has a current value with an optional equation string.
I have 2 textboxes:
One (a) where you can enter an equation shortcut to a value to put into the other (b).
(b) contains the actual value.
(for example, in (a), if you enter 'pi', the second will then fill with "3.1415926535897931")
I'm using 2 textboxes so the user can refine their equation if they need to, and watch the value change as they modify it.
The data has 2 fields, one being the equation string and the other being the current value.
so I have (a).Text bound to the string, a new property on (a) that holds the value, and I bind (b).Text to the value also.
(a).Text is TwoWay
(a).Value is OneWayToSource (since changes to the text should only be pushed to b)
(b).Value is TwoWay
This all works fine if I have the data set in the constructor before any XAML binding, but does not work at all if I add the data after binding.
Here is a minimal amount of code that shows the problem.
The only comment is at the line that can make it work or not.
As a last resort I could turn it into a custom control and handle it in the code-behind, but I'd think this should work in the first place.
Any ideas why this isn't working?
Thanks!
Here is the XAML:
<Window x:Class="twoBindingsOnSameField.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:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:twoBindingsOnSameField"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<Button Content="load data" Click="Button_Click" Width="80" IsEnabled="{Binding NeedsData}"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="enter text:" Width="80"/>
<local:TextBoxCalc Text="{Binding Item.ItemString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
TextBoxCalculatedValue="{Binding Item.ItemValue, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}"
Width="200"
IsEnabled="{Binding HasData}"
/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="updated text:" Width="80"/>
<TextBox Text="{Binding Item.ItemValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Width="200"
IsEnabled="{Binding HasData}"
/>
</StackPanel>
</StackPanel>
</Window>
Here is the codebehind.
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace twoBindingsOnSameField
{
public partial class MainWindow : Window
{
data data;
public MainWindow()
{
InitializeComponent();
data = new data();
/// ---- Does not work with the following line commented out, but does if it is uncommented ----
/// ---- use the button to set the data ----
//setdata();
DataContext = data;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
setdata();
}
void setdata()
{
if (data.Item == null)
data.Item = new dataitem();
}
}
public class data : notifybase
{
dataitem item;
public data()
{
}
public dataitem Item
{
get
{
return item;
}
set
{
if (item != value)
{
item = value;
notifyPropertyChanged("Item");
notifyPropertyChanged("HasData");
notifyPropertyChanged("NeedsData");
}
}
}
public bool HasData
{
get
{
return Item != null;
}
}
public bool NeedsData
{
get
{
return Item == null;
}
}
}
public class dataitem : notifybase
{
string itemString;
string itemValue;
public dataitem()
{
itemString = "3";
itemValue = "4";
}
public virtual string ItemString
{
get
{
return this.itemString;
}
set
{
if (!object.Equals(this.itemString, value))
{
this.itemString = value;
notifyPropertyChanged("ItemString");
}
}
}
public virtual string ItemValue
{
get
{
return this.itemValue;
}
set
{
if (!object.Equals(this.itemValue, value))
{
this.itemValue = value;
notifyPropertyChanged("ItemValue");
}
}
}
}
public class TextBoxCalc : TextBox
{
public TextBoxCalc()
{
TextProperty.AddHandler(this, (o,e)=>TextBoxCalculatedValue="updated:" + Text);
}
#region TextBoxCalculatedValue
public static DependencyProperty TextBoxCalculatedValueProperty = DependencyProperty.Register("TextBoxCalculatedValue", typeof(string), typeof(TextBoxCalc), new PropertyMetadata(""));
public string TextBoxCalculatedValue
{
get
{
return (string)GetValue(TextBoxCalculatedValueProperty);
}
set
{
if (!object.Equals(TextBoxCalculatedValue, value))
SetValue(TextBoxCalculatedValueProperty, value);
}
}
#endregion
}
public class notifybase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
protected virtual void notifyPropertyChanged(string propertyName)
{
PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
OnPropertyChanged(e);
}
}
static class extensions
{
public static void AddHandler(this DependencyProperty prop, object component, EventHandler handler)
{
DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(prop, component.GetType());
if (dpd != null)
dpd.AddValueChanged(component, handler);
}
}
}
The reason why it works when you uncomment //setdata(); is because it is initializing the object in what is effectively your viewmodel, therefore you can change its properties via binding. To clarify as a side note, data would be your view model, and dataitem is your model, however you're dataitem is using INPC, so it doesn't really make sense in this case to have a viewmodel necessarily.
Anyways, the issue is that TextBoxCalculatedValue is set to a OneWayToSource binding. When you run the code commented out, its going to try and bind to a null value. When it does, it tries to update a null value, which isn't possible. WPF handles what would normally be a null exception automatically. When you update the dataItem by clicking the button, it doesn't update the object TextBoxCalc is bound to, so instead, it will continue trying to bind & update the null object. Change it to a TwoWay binding and you'll see a difference. Changing to TwoWay is probably your best option.
Good practice is to use constructor injection to practice dependency injection. With that being said, passing a dataItem to data would be the best route, and at the very least, initializing dataItem in data's constructor would be an ideal approach. So,
public data(dataItem item)
{
Item = item;
}
or
public data()
{
Item = new dataitem();
}

ComboBox two way binding to selecteditem

I have spent considerable amount of time investigating this problem. Any help would be greatly appreciated.
I have a WPF ComboBox declared like this.
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Click="Button_Click">Click Me</Button>
<ComboBox ItemsSource="{Binding ListOfValues}" SelectedItem="{Binding MySelectedItem}" Grid.Row="1">
</ComboBox>
<CheckBox IsChecked="{Binding IsValueChecked}" Grid.Row="2"></CheckBox>
</Grid>
In my code behind, i have these properties and i am implementing the INotifyPropertyChanged
public Window1()
{
InitializeComponent();
ListOfValues = new List<string>();
ListOfValues.Add("apple");
ListOfValues.Add("ball");
ListOfValues.Add("cat");
ListOfValues.Add("dog");
MySelectedItem = "cat";
IsValueChecked = true;
}
public List<string> ListOfValues
{
get
{
return _listOfValues;
}
set
{
_listOfValues = value;
OnPropertyChanged("ListOfValues");
}
}
public string MySelectedItem
{
get
{
return _selectedValueString;
}
set
{
_selectedValueString = value;
OnPropertyChanged("MySelectedItem");
}
}
public bool IsValueChecked
{
get
{
return _isVlaueChanged;
}
set
{
_isVlaueChanged = value;
OnPropertyChanged("IsValueChecked");
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
MySelectedItem = "dog";
IsValueChecked = !IsValueChecked;
}
The button click event changes the MySelectedItem which is bound to the SelectedItem property of the combobox. But upon the button click nothing gets selected in the combobox. I dont understand why. This happens even if i set explicitly Mode=TwoWay. Please suggest. Note that my datacontext is set to self, so i have confirmed that data binding is happening properly by adding a checkbox
EDIT: Note that this happens in a sample WPF project. But my original project where i want this to work is a winforms app. I am using the elementhost to embed my wpf control. Is that making a difference?
The selected item needs to be set to an object in the list you have it bound to. settings it to a string with a matching value won't work. So try this:
foreach(string animal in ListOfValues)
{
if( animal == "dog")
this.MySelectedItem = animal;
}
I tried to reproduce your problem and I have some questions. Can you please show me your implementation of OnPropertyChanged? When I have a look at the MSDN (http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.onpropertychanged.aspx) this function requires a DependencyPropertyChangedEventArgs as the first parameter, not a string. And in addition, OnPropertyChanged is for notifying about changes in Dependency Properties, not for normal properties.
So I think you overloaded that method to support INotifyPropertyChanged, right?
I tried to implement a working example, this is the result:
public partial class TestWindow2 : Window, INotifyPropertyChanged
{
public TestWindow2()
{
InitializeComponent();
ListOfValues = new List<string> { "apple", "ball", "cat", "dog" };
MySelectedItem = "cat";
IsValueChecked = true;
this.DataContext = this;
}
...
public string MySelectedItem
{
get
{
return _selectedValueString;
}
set
{
_selectedValueString = value;
RaisePropertyChanged("MySelectedItem");
}
}
...
private void Button_Click(object sender, RoutedEventArgs e)
{
MySelectedItem = "dog";
IsValueChecked = !IsValueChecked;
}
private void RaisePropertyChanged(String name)
{
if( this.PropertyChanged != null ) this.PropertyChanged(this, new PropertyChangedEventArgs(name));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Works perfectly for me. When I click the button, dog becoms the selected item in the combo box and the checkbox toggles its state.
If your items are a reference type (and you are just using string for an example), check that the Equals() method is returning what you expect. You might need to override the Equals method (eg this.ID ==other.ID or something like that) to get the correct behavior.

IndexOutOfRangeException when changing selected TabItem twice

I have the following simple WPF-app:
<Window x:Class="TabControlOutOfRangeException.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">
<TabControl ItemsSource="{Binding ItemsSource}"
SelectedIndex="{Binding SelectedIndex, IsAsync=True}" />
</Window>
with following simple code-behind:
using System.Collections.Generic;
namespace TabControlOutOfRangeException
{
public partial class MainWindow
{
public List<string> ItemsSource { get; private set; }
public int SelectedIndex { get; set; }
public MainWindow()
{
InitializeComponent();
ItemsSource = new List<string>{"Foo", "Bar", "FooBar"};
DataContext = this;
}
}
}
When I click on the second tab ("Bar"), nothing is displayed. When I click again on any tab, I get an IndexOutOfRangeException. Setting IsAsync to False, the TabControl works.
Unfortunately, I have the requirement to query the user a "Save changes?" question when he leaves the current tab. So I wanted to set the SelectedIndex back to the old value within the set-property. Obviously this doesn't work. What am I doing wrong?
Update
I've subclassed the TabControl with the evil hack and it works for me. Here is the code of MainWindow.xaml:
<Window x:Class="TabControlOutOfRangeException.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:TabControlOutOfRangeException="clr-namespace:TabControlOutOfRangeException" Title="MainWindow" Height="350" Width="525">
<Grid>
<TabControlOutOfRangeException:PreventChangingTabsTabControl
ItemsSource="{Binding ItemsSource}"
SelectedIndex="{Binding SelectedIndex}"
CanChangeTab="{Binding CanChangeTab}" Margin="0,0,0,51" />
<CheckBox Content="CanChangeTab" IsChecked="{Binding CanChangeTab}" Margin="0,287,0,0" />
</Grid>
</Window>
And here MainWindow.xaml.cs:
using System.Collections.Generic;
using System.ComponentModel;
namespace TabControlOutOfRangeException
{
public partial class MainWindow : INotifyPropertyChanged
{
public int SelectedIndex { get; set; }
public List<string> ItemsSource { get; private set; }
public MainWindow()
{
InitializeComponent();
ItemsSource = new List<string> { "Foo", "Bar", "FooBar" };
DataContext = this;
}
private bool _canChangeTab;
public bool CanChangeTab
{
get { return _canChangeTab; }
set
{
_canChangeTab = value;
OnPropertyChanged("CanChangeTab");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string property)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(property));
}
}
}
And finally the subclassed TabControl:
using System;
using System.Windows;
using System.Windows.Controls;
namespace TabControlOutOfRangeException
{
public class PreventChangingTabsTabControl : TabControl
{
private int _previousTab;
public PreventChangingTabsTabControl()
{
SelectionChanged += (s, e) =>
{
if (!CanChangeTab)
{
e.Handled = true;
SelectedIndex = _previousTab;
}
else
_previousTab = SelectedIndex;
};
}
public static readonly DependencyProperty CanChangeTabProperty = DependencyProperty.Register(
"CanChangeTab",
typeof(Boolean),
typeof(PreventChangingTabsTabControl)
);
public bool CanChangeTab
{
get { return (bool)GetValue(CanChangeTabProperty); }
set { SetValue(CanChangeTabProperty, value); }
}
}
}
I'd consider a redesign of that window instead of introducing a heap of new problems by just trial-and-erroring on the "IsAsync" property of the binding.
I am not sure if a tab control will allow this level of control you seek. You could try to catch the event when someone tries to change the selected item, but you would not be able to cancel it out. There is a way however, see Option 4 if you dont want to read the other suggestions.
Option 1: The custom control
I would consider writing a bit of custom code that mimics the functionality of an item container. Its easy to achieve your desired behaviour this way. Just bind a command to the buttons (or whatever control you wish the user to click on), and return CanExecute with false if there are still changes to be submitted - or ask your user whatever you want when it gets executed, and only change the content displayed (ie your custom "TabItem") if desired.
Option 2: Preventing the user by disabling the tabs
Another way would be to bind the "IsEnabled" property of each of the tabitems to a dependency property on your viewmodel, that controls which of them is available to the user. Like, you know that the first page still needs work, just disable all the other ones meanwhile. But be aware that right now you are not creating any TabItems - your content are just plain strings.
public List<TabItem> ItemsSource { get; private set; }
....
ItemsSource = new List<TabItem> { new TabItem() { Header = "Foo", Content = "Foo" }, new TabItem() { Header = "Bar", Content = "Bar" }, new TabItem() { Header = "FooBar", Content = "FooBar" } };
Since you don't want to prevent the user doing something but rather would like to ask to save the changes, i'd go for the custom control route. Still there is option 3.
Option 3: Popup window
Use a popup window and ask to save changes if the user is finished with changing whatever is on that page and clicks on the "Close" button (rather than the "Save" button that should also reside on the same page ;) )
Option 4: Check on StackOverflow
Actually i did that for you, and here is a solution another user has found for the exact same problem: WPF Tab Control Prevent Tab Change
The reason why i didnt post that up-front was that i personally wouldnt do it that way because, man do i HATE applications that do this.
Here you go.
Try actually implementing the SelectedIndex
namespace TabControlOutOfRangeException
{
public partial class MainWindow
{
public List<string> ItemsSource { get; private set; }
private int selectedIndex
public int SelectedIndex {
get { return selectedIndex; }
set { selecectedIndex = value; } }
public MainWindow()
{
InitializeComponent();
ItemsSource = new List<string>{"Foo", "Bar", "FooBar"};
DataContext = this;
}
}
}
If you want to be able to affect the TabControl the binding needs to be two-way, i.e. your code-behind needs to be able to notify the view that the property changed, for that you should implement INotifyPropertyChanged in your window, e.g.
public partial class MainWindow : INotifyPropertyChanged
private int _selectedIndex;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
if (_selectedIndex != value)
{
_selectedIndex = value;
OnPropertyChanged("SelectedIndex");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Async bindings are usually for properties which have a long-running getter, with e.g. a database query, you should not need this here.
In case you want to to change the selectedIndex in the setter itself, then to get it updated on UI, you have to raise the property changed in an async manner like this -
public partial class MainWindow : INotifyPropertyChanged
private int _selectedIndex;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
if (_selectedIndex != value)
{
_selectedIndex = value;
OnPropertyChangedAsAsync("SelectedIndex");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
protected virtual void OnPropertyChangedAsAsync(string propertyName)
{
Dispatcher.CurrentDispatcher.BeginInvoke((Action)delegate { OnPropertyChanged(propertyName); }, DispatcherPriority.Render, null);
}

WPF: Nested DependencyProperties

I have an ObservableCollection of "Layouts" and a "SelectedLocation" DependencyProperty on a Window. The SelectedLocation has a property called "Layout", which is an object containing fields like "Name" etc. I'm trying to bind a combobox to the SelectedLayout but it's not working.
The following does not work, I've tried binding to SelectedItem instead to no avail. I believe it may be something to do with the fact that I'm binding to a subProperty of the SelectedLocation DependencyProperty (though this does implement INotifyPropertyChanged.
<ComboBox Grid.Row="2" Grid.Column="0" x:Name="cboLayout" ItemsSource="{Binding Layouts,ElementName=root}" SelectedValue="{Binding SelectedLocation.Layout.LayoutID,ElementName=root}" DisplayMemberPath="{Binding Name}" SelectedValuePath="LayoutID" />
However, the following works (Also bound to the "SelectedLocation" DP:
<TextBox Grid.Row="4" Grid.Column="1" x:Name="txtName" Text="{Binding SelectedLocation.Name,ElementName=root,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" />
What type property Layouts has? I suppose something like this this: IEnumerable<Layout>.
But you bind selected value to Layout.LayoutID. So you got situation, when combo box contains Layout objects, and you try to select it by Int identifier. Of course binding engine can't find any Int there.
I have no idea about details of your code, so one thing I could propose: try to reduce your binding expression: SelectedItem="{Binding SelectedLocation.Layout,ElementName=root}.
If no success, provide more code to help me understand what's going on.
====UPDATE====
As I've said, you are obviously doing something wrong. But I am not paranormalist and couldn't guess the reason of your fail (without your code). If you don't want to share your code, I decided to provide simple example in order to demonstrate that everything works. Have a look at code shown below and tell me what is different in your application.
Class Layout which exposes property LayoutId:
public class Layout
{
public Layout(string id)
{
this.LayoutId = id;
}
public string LayoutId
{
get;
private set;
}
public override string ToString()
{
return string.Format("layout #{0}", this.LayoutId);
}
}
Class SelectionLocation which has nested property Layout:
public class SelectedLocation : INotifyPropertyChanged
{
private Layout _layout;
public Layout Layout
{
get
{
return this._layout;
}
set
{
this._layout = value;
this.OnPropertyChanged("Layout");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
var safeEvent = this.PropertyChanged;
if (safeEvent != null)
{
safeEvent(this, new PropertyChangedEventArgs(name));
}
}
}
And Window class with dependency properties (actually, in my example StartupView is UserControl, but it doesn't matter):
public partial class StartupView : UserControl
{
public StartupView()
{
InitializeComponent();
this.Layouts = new Layout[] { new Layout("AAA"), new Layout("BBB"), new Layout("CCC") };
this.SelectedLocation = new SelectedLocation();
this.SelectedLocation.Layout = this.Layouts.ElementAt(1);
}
public IEnumerable<Layout> Layouts
{
get
{
return (IEnumerable<Layout>)this.GetValue(StartupView.LayoutsProperty);
}
set
{
this.SetValue(StartupView.LayoutsProperty, value);
}
}
public static readonly DependencyProperty LayoutsProperty =
DependencyProperty.Register("Layouts",
typeof(IEnumerable<Layout>),
typeof(StartupView),
new FrameworkPropertyMetadata(null));
public SelectedLocation SelectedLocation
{
get
{
return (SelectedLocation)this.GetValue(StartupView.SelectedLocationProperty);
}
set
{
this.SetValue(StartupView.SelectedLocationProperty, value);
}
}
public static readonly DependencyProperty SelectedLocationProperty =
DependencyProperty.Register("SelectedLocation",
typeof(SelectedLocation),
typeof(StartupView),
new FrameworkPropertyMetadata(null));
}
XAML of StartupView:
<UserControl x:Class="Test.StartupView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:self="clr-namespace:HandyCopy"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="Root">
<WrapPanel>
<ComboBox ItemsSource="{Binding Path=Layouts,ElementName=Root}"
SelectedItem="{Binding Path=SelectedLocation.Layout, ElementName=Root}"/>
</WrapPanel>
</UserControl>

Resources