WPF lost Databinding
https://stackoverflow.com/questions/642010
Question
I'm new to WPF and its Databinding, but I stumbled upon a strange behaviour I could not resolve for myself.
In a Dialog I've got a Listbox with Users and a TextBox for a username. Both are bound to a UserLogonLogic-which publishes among others a CurrentUser
property.
I want the TextBox to update its text when I click on a name in the ListBox. I also want the SelectedItem
in the ListBox to be updated when I enter a username directly into the TextBox. Partial names in the TextBox will be resolved to the first matching value in the listbox or null if there is none.
At first the TextBox gets updated every time I click into the ListBox. Debug shows me that every time the PropertyChangeEvent
for CurrentUser
is fired the method txtName_TextChanged
method is called. Only after I have typed something into the textbox the DataBinding
of the TextBox seems to be lost. There will be no further updates of the TextBox when I click into the ListBox. Debug now shows me that the method txtName_TextChanged
is no longer being called after the CurrentUser
PropertyChangeEvent
is fired.
Does anybody have an idea where I could have gone wrong?
Thanks a lot
Rü
UserLogon.xaml:
<ListBox Grid.Column="0" Grid.Row="1" Grid.RowSpan="4" MinWidth="100" Margin="5" Name="lstUser" MouseUp="lstUser_MouseUp"
ItemsSource="{Binding Path=Users}" SelectedItem="{Binding Path=CurrentUser, Mode=TwoWay}"/>
<TextBox Grid.Column="1" Grid.Row="1" Margin="3" Name="txtName" TextChanged="txtName_TextChanged"
Text="{Binding Path=CurrentUser, Mode=OneWay}" />
UserLogon.xaml.cs:
public UserLogon()
{
InitializeComponent();
_logic = new UserLogonLogic();
TopLevelContainer.DataContext = _logic;
}
private int _internalChange = 0;
private void txtName_TextChanged(object sender, TextChangedEventArgs e)
{
if (_internalChange > 0)
{
return;
}
_internalChange++;
string oldName = txtName.Text;
User user = _logic.SelectByPartialUserName(oldName);
string newName = (user == null) ? "" : user.Name;
if (oldName != newName)
{
txtName.Text = (newName == "") ? oldName : newName;
txtName.Select(oldName.Length, newName.Length);
}
_internalChange--;
}
UserLogon.Logic.cs:
public class UserLogonLogic : INotifyPropertyChanged
{
private User _currentUser;
public User CurrentUser
{
get { return _currentUser; }
set
{
if (value != CurrentUser)
{
_currentUser = value;
OnPropertyChanged("CurrentUser");
}
}
private IEnumerable<User> _users;
public IEnumerable<User> Users
{
get
{
if (_users == null)
{
List<User> _users = Database.GetAllUsers();
}
return _users;
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string prop)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
public User SelectByPartialUserName(string value)
{
if (value != "")
{
IEnumerable<User> allUser = GetAllUserByName(value);
if (allUser.Count() > 0)
{
CurrentUser = allUser.First();
}
else
{
CurrentUser = null;
}
}
else
{
CurrentUser = null;
}
return CurrentUser;
}
private IEnumerable<User> GetAllUserByName(string name)
{
return from user in Users
where user.Name.ToLower().StartsWith(name.ToLower())
select user;
}
}
Solution
This is a job for a good view model. Define two properties on your view model:
SelectedUser : User
UserEntry : string
Bind the ListBox
's SelectedItem
to the SelectedUser
property, and the TextBox
's Text
property to the UserEntry
property. Then, in your view model you can do the work to keep them in sync:
- if SelectedUser
changes, set UserEntry
to that user's Name
- if UserEntry
changes, do an intelligent search through all users and set SelectedUser
to either null
if no match was found, or the first matching User
Here is a complete and working sample. I wish I could easily attach a zip file right about now.
First, ViewModel.cs:
public abstract class ViewModel : INotifyPropertyChanged
{
private readonly Dispatcher _dispatcher;
protected ViewModel()
{
if (Application.Current != null)
{
_dispatcher = Application.Current.Dispatcher;
}
else
{
_dispatcher = Dispatcher.CurrentDispatcher;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected Dispatcher Dispatcher
{
get { return _dispatcher; }
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, e);
}
}
protected void OnPropertyChanged(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
}
User.cs:
public class User : ViewModel
{
private readonly string _name;
public User(string name)
{
_name = name;
}
public string Name
{
get { return _name; }
}
}
LogonViewModel.cs:
public class LogonViewModel : ViewModel
{
private readonly ICollection<User> _users;
private User _selectedUser;
private string _userEntry;
public LogonViewModel()
{
_users = new List<User>();
//fake data
_users.Add(new User("Kent"));
_users.Add(new User("Tempany"));
}
public ICollection<User> Users
{
get { return _users; }
}
public User SelectedUser
{
get { return _selectedUser; }
set
{
if (_selectedUser != value)
{
_selectedUser = value;
OnPropertyChanged("SelectedUser");
UserEntry = value == null ? null : value.Name;
}
}
}
public string UserEntry
{
get { return _userEntry; }
set
{
if (_userEntry != value)
{
_userEntry = value;
OnPropertyChanged("UserEntry");
DoSearch();
}
}
}
private void DoSearch()
{
//do whatever fuzzy logic you want here - I'm just doing a simple match
SelectedUser = Users.FirstOrDefault(user => user.Name.StartsWith(UserEntry, StringComparison.OrdinalIgnoreCase));
}
}
UserLogon.xaml:
<UserControl x:Class="WpfApplication1.UserLogon"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300" Width="300">
<StackPanel>
<ListBox ItemsSource="{Binding Users}" SelectedItem="{Binding SelectedUser}" DisplayMemberPath="Name"/>
<TextBox Text="{Binding UserEntry, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
</UserControl>
UserLogon.xaml.cs:
public partial class UserLogon : UserControl
{
public UserLogon()
{
InitializeComponent();
//would normally map view model to view with a DataTemplate, not manually like this
DataContext = new LogonViewModel();
}
}