views:

437

answers:

2

Hello there,

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 reloved 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

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;
    }


}
A: 

shouldn't your textbox have twoway-binding?

Natrium
Actually I dont think it should, because I actively set the TextChanged attrib. so I only use oneway Binding to ensure that I always "passively" read the value should CurrentUser change. All active changes in the TextBox are handled by txtName_TextChanged.
Savinien
I tried it with TwoWay - the only result I get is that now the program wont work at all properly. Right from the start changes with the SelectedItem aren't reflected in the TextBox.
Savinien
+4  A: 

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();
 }
}

HTH, Kent

Kent Boogaart