locked
How to prevent Auto Complete Box from leaving invalid values when user leaves field and/or how to replace Auto Complete Box with ComboBox? RRS feed

  • Question

  • I have an entity where a property is constrained by a foreign key.   The autocomplete box on the New Data screen properly shows the drop down, however it allows the user to type text into the Auto Complete Field that does not exist in the drop down and if they tab to the next field it leaves the bogus text in the field instead of setting it back to the value it had prior to the user typing in the invalid text.  I would expect the behavior to be that once the user leaves the field, that the value of the Auto Complete Box should be either a newly selected valid value from the list, or it would revert and display the previous value if one existed otherwise be blank or null.  Perhaps there could be an option for "AllowUnknownValues", for those cases where people want to automatically add new values into a list if they are not already there, but I suspect that is more the exception than the norm. 

    As an example, say I have a Contact Entity containing a country field constrained by foreign key to a list of countries.  If the current country value was "United States", and I type in "Bogus" in the country autocomplete box and hit tab to the next field.  Now the field contains "Bogus", even though the property value and the underlying field value on the entity have not changed.  If I switch to another screen and then come back, the value resets as expected and "United States" will be displayed again, but only when I leave the screen.  These seems confusing to me, and is not something I would expect my users to understand nor would I want to explain to them. Is this a bug or is there a way to make it behave the way I would expect?

    At first I thought I could fix the problem by wiring up the validation event, but since the property does not actually change when you just tab or click out of the field, none of the normal events fire.  I then thought I could fix this by handling a Leaving or Blur event, but if one exists, wiring it up is not intuitive, I haven't found an example yet, and I really would only want to take action if the text entered into the auto complete box changes, not is someone clicks in and out or tabs through it.  Granted I'm trying to learn silverlight at the same time as lightswitch so maybe this would be intuitive if I was already was a Silverlight expert, but unfortunately I'm not yet.

    So then I figure ok, I'll just replace the Auto Complete Box with a Custom Control, and use a Silverlight ComboBox however I've looked at (http://msdn.microsoft.com/en-us/library/gg406736.aspx#LSBinding)  but I'm not sure of the following:

    • What is the best screen event to setup the databinding in?  InitializeDataWorkspace, Created, Activated, other?  Why?
    • If the original Choices property on the Auto Complete Box was set to Auto, what string path do I use when binding the ComboBox.ItemsSourceProperty, or do I always need to add a new Screen Data Item to serve as the source for the ComboBox Items?
    • What property should I bind to for the selection? SelectedItemProperty, SelectedValuePathProperty or SelectedValueProperty? 
    • What should I set the path to, and how do I know what the valid list of paths are?
      Below is what I tried but it didn't work.   I hate having to ask this question, because I'm probably making a stupid mistake, but I've wasted so much time on what seems like such a trivial problem, that it is either harder than it should be, or I'm missing something that I assume should be obvious.
      partial void NewContactScreen_Created()
      {
       IContentItemProxy ctrlProxy = this.FindControl("Country"); 
       if (ctrlProxy != null)
       {
       // These bindings do not work. I assume the path argmuments are not correct, but I don't know what values to use
       ctrlProxy.SetBinding(ComboBox.ItemsSourceProperty, "Items", BindingMode.TwoWay);
       ctrlProxy.SetBinding(ComboBox.SelectedItemProperty, "SelectedItem", BindingMode.TwoWay);
       }
      }
      
      

    Anyway, when I couldn't get that to work, I wired up the ControlAvailable event, got the ComboBox from event.Control and was able to bind the ItemSource to the list I wanted by adding a new Countries Data Item to the screen.  Then I tried to setup a 2 way binding on the SelectedItemProperty, but I couldn't get that to work.  I was able to wire up the ComboBox SelectionChanged event, and set the ContactProperty.Country property from there, but that doesn't solve the problem of updating the ComboBox SelectedItem when the Country changes on the Contact entity.  Below is the sample code:

    partial void NewContactScreen_Created()
    {
     IContentItemProxy ctrlProxy = this.FindControl("Country");
     if (ctrlProxy != null)
     {
     ctrlProxy.ControlAvailable += this.OnCountryControlAvailable;
     }
    }
    
    void OnCountryControlAvailable(object sender, ControlAvailableEventArgs e)
    {
     //Initialize Control
     ComboBox cb = e.Control as ComboBox;
     cb.ItemsSource = this.Countries; // this works to bind ComboBox to list of Countries, but I had to add a separate Data Item to screen
                     // which I didn't have to do using the Auto Complete Box
    
     cb.SelectionChanged += new SelectionChangedEventHandler(OnCountrySelectionChanged);
    
     // This does not seem to work, as the ComboBox SelectedItem does not get set to the Entity Value nor will it set the value
     // It also doesn't work using "ContactProperty.Country"
     Binding b = new Binding("ContactProperty.Country"); // new Binding("Contact.Country") doesn't work either
     b.Mode = BindingMode.TwoWay;
     cb.SetBinding(ComboBox.SelectedItemProperty, b);
    }
    
    void OnCountrySelectionChanged(object sender, SelectionChangedEventArgs e)
    {
      var cb = sender as ComboBox;
      this.ContactProperty.Country = cb.SelectedItem as Country;
    }
    

     

    Any insights on how I can resolve either or both of these issues would be greatly appreciated.

    Thanks,

     

     


    Rick
    Monday, May 9, 2011 2:56 AM

Answers

  • Hi Rick,

    In answer to the main question that you have, here's a method which I think will work for you.

    On your contact screen, create a new query to return a list of Countries. To do this, click on the 'Add Data Item' button, select the Query radio button and select 'Countries - Country (All)'

    The SetBindings syntax would then look something like this: 

    partial void NewContactScreen_Created()
    {
     IContentItemProxy ctrlProxy = this.FindControl("Country"); 
     if (ctrlProxy != null)
     {
     // These bindings do not work. I assume the path argmuments are not correct, but I don't know what values to use
     ctrlProxy.SetBinding(ComboBox.ItemsSourceProperty, "Screen.Countries", BindingMode.TwoWay);
     ctrlProxy.SetBinding(ComboBox.SelectedItemProperty, "Screen.ContactProperty.Country", BindingMode.TwoWay);
     }
    }
    
    


    Hope that's of some use,

    Tim

    • Marked as answer by RickWassum Monday, May 9, 2011 1:53 PM
    Monday, May 9, 2011 1:00 PM
  • Hi Yann,

    Thanks for the suggestion, I had actually read that post, but was not thinking of it in this context at the time, so the lightbulb did not go off.  I sure envy people with a photographic memory.  Anyway, using that example, I was able to get it to work exactly as I wanted.  There are a few subtle things to be aware of when doing it.  I've included the commented code below so that hopefully someone else will find it useful.  I would contend that this is how the control should behave out of the box, without requiring this much work, but I realize that "to ship is to choose", so at least it is possible.

    //
    // The following code example shows how to wire up a LightSwitch AutoCompleteBox so that
    // when the user leaves the control either by tabbing out, or clicking outside the box
    // the displayed text will automatically revert to the value of the current value of the property.
    //
    partial void NewContactScreen_Created()
    {
    	// setup callback for when control is available on screen
    	this.FindControl("Country").ControlAvailable += this.OnCountryControlAvailable;
    }
    
    void OnCountryControlAvailable(object sender, ControlAvailableEventArgs e)
    {
    	var c = e.Control as System.Windows.Controls.Control;
    	// Now that control is available, setup callback for when control looses focus
    	c.LostFocus += new System.Windows.RoutedEventHandler(OnCountryControlLostFocus);
    }
    
    void OnCountryControlLostFocus(object sender, System.Windows.RoutedEventArgs e)
    {
    	// NOTE: System.Windows.Controls.AutoCompleteBox requires you add a reference 
    	//    to System.Windows.Controls.Input in the Client References section of your project
    	//    It is not there by default
    	var acb = sender as System.Windows.Controls.AutoCompleteBox;
    	string txtComboText = acb.Text;
    
    	// We need to switch to the Logic Thread to check our combo text against the list of valid values
    	// TODO: You must compare your combo text against the property displayed by default in the drop down
    	//    I'm not sure how to automatically get that off hand, but I assume it is possible. 
    	this.Details.Dispatcher.BeginInvoke(() =>
    	{
    		if (this.ContactProperty.Country.CountryName != txtComboText) // Note: how the txtComboText variable is available to the dispatched thread
    		{
    			var country = this.DataWorkspace.ContactDataSource.Countrys.Where(gl => gl.CountryName == txtComboText).FirstOrDefault();
    			if (country == null) // text in combobox is not a valid selection
    			{
    				// We need to get back to the UI thread to access the Auto Complete Box and set its property
    				// otherwise we get a cross threading exception
    				Microsoft.LightSwitch.Threading.Dispatchers.Main.BeginInvoke(() =>
    				{
    					// NOTE: this example is using ContactProperty on a New Data Screen to get the current value
    					//    from a details screen you would probably use this.Contact.Country.CountryName
    					//    Obviously the exact names depend on your entities and how you name them.
    					acb.Text = this.ContactProperty.Country.CountryName; // Note again: how the acb variable is also available here.
    				});
    			}
    		}
    	});
    }
    
    

    Thanks to all who eventually helped me get to this solution...  Also check Tim Leung's post which answers the ComboBox databinding question.

     

     

     


    Rick
    • Marked as answer by RickWassum Monday, May 9, 2011 4:07 PM
    Monday, May 9, 2011 4:07 PM

All replies

  • As random note, they can also hit Esc key to bring back old list value.
    Monday, May 9, 2011 8:47 AM
  • Hi Rick,

    Just going back to your original situatio, the way I handle it is to use the control's LostFocus event handler.

    Have a look at Tim's blog post, you may find it's easier to do what you wanted than you thought.

    http://dotnettim.wordpress.com/2011/04/20/lightswitch-add-non-existent-records-using-autocompletebox/

    I noticed that you test for null after executing "IContentItemProxy ctrlProxy = this.FindControl("Country");".

    While I agree with you that would be the most normal thing to expect if the control doesn't exist, but in fact FindControl will throw an exception if the control isn't found, it doesn't return null.

    Yann

    • Proposed as answer by Yann Duran Monday, May 9, 2011 11:35 PM
    Monday, May 9, 2011 10:57 AM
  • Hi Rick,

    I have not seen an example of setting binding for a combobox in lightswitch. For binding by modification of control xml see:http://edulorenzo.wordpress.com/2011/03/08/adding-a-user-control-to-a-visual-studio-lightswitch-application/

    Rick at Spursoft has extensions that include a combo box:http://archive.msdn.microsoft.com/SpursoftLSControls

    Your question is an interesting one. I would like to know the expressions to use to get the binding to work from within lightswitch without a custom control or an additional data collection.

    Mike

    Monday, May 9, 2011 11:20 AM
  • Hi Rick,

    In answer to the main question that you have, here's a method which I think will work for you.

    On your contact screen, create a new query to return a list of Countries. To do this, click on the 'Add Data Item' button, select the Query radio button and select 'Countries - Country (All)'

    The SetBindings syntax would then look something like this: 

    partial void NewContactScreen_Created()
    {
     IContentItemProxy ctrlProxy = this.FindControl("Country"); 
     if (ctrlProxy != null)
     {
     // These bindings do not work. I assume the path argmuments are not correct, but I don't know what values to use
     ctrlProxy.SetBinding(ComboBox.ItemsSourceProperty, "Screen.Countries", BindingMode.TwoWay);
     ctrlProxy.SetBinding(ComboBox.SelectedItemProperty, "Screen.ContactProperty.Country", BindingMode.TwoWay);
     }
    }
    
    


    Hope that's of some use,

    Tim

    • Marked as answer by RickWassum Monday, May 9, 2011 1:53 PM
    Monday, May 9, 2011 1:00 PM
  • Hi Rick,

    In answer to the main question that you have, here's a method which I think will work for you.

    On your contact screen, create a new query to return a list of Countries. To do this, click on the 'Add Data Item' button, select the Query radio button and select 'Countries - Country (All)'

    The SetBindings syntax would then look something like this: 

    partial void NewContactScreen_Created()
    
    {
    
     IContentItemProxy ctrlProxy = this.FindControl("Country"); 
    
     if (ctrlProxy != null)
    
     {
    
     // These bindings do not work. I assume the path argmuments are not correct, but I don't know what values to use
    
     ctrlProxy.SetBinding(ComboBox.ItemsSourceProperty, "Screen.Countries", BindingMode.TwoWay);
    
     ctrlProxy.SetBinding(ComboBox.SelectedItemProperty, "Screen.ContactProperty.Country", BindingMode.TwoWay);
    
     }
    
    }
    
    
    
    


    Hope that's of some use,

    Tim


    Thanks Tim!!!  You do not know how much I appreciate that simple example.  That works exactly as I wanted it too and it was a very easy solution, which is what I figured it should be.  It seems obvious now that I see it.  Hopefully this example will save others some time.  I will mark your post as the answer, at least to the part about binding to a combobox. 
    Rick
    Monday, May 9, 2011 1:53 PM
  • Glad it works Rick!

    Tim

    Monday, May 9, 2011 2:22 PM
  • Hi Yann,

    Thanks for the suggestion, I had actually read that post, but was not thinking of it in this context at the time, so the lightbulb did not go off.  I sure envy people with a photographic memory.  Anyway, using that example, I was able to get it to work exactly as I wanted.  There are a few subtle things to be aware of when doing it.  I've included the commented code below so that hopefully someone else will find it useful.  I would contend that this is how the control should behave out of the box, without requiring this much work, but I realize that "to ship is to choose", so at least it is possible.

    //
    // The following code example shows how to wire up a LightSwitch AutoCompleteBox so that
    // when the user leaves the control either by tabbing out, or clicking outside the box
    // the displayed text will automatically revert to the value of the current value of the property.
    //
    partial void NewContactScreen_Created()
    {
    	// setup callback for when control is available on screen
    	this.FindControl("Country").ControlAvailable += this.OnCountryControlAvailable;
    }
    
    void OnCountryControlAvailable(object sender, ControlAvailableEventArgs e)
    {
    	var c = e.Control as System.Windows.Controls.Control;
    	// Now that control is available, setup callback for when control looses focus
    	c.LostFocus += new System.Windows.RoutedEventHandler(OnCountryControlLostFocus);
    }
    
    void OnCountryControlLostFocus(object sender, System.Windows.RoutedEventArgs e)
    {
    	// NOTE: System.Windows.Controls.AutoCompleteBox requires you add a reference 
    	//    to System.Windows.Controls.Input in the Client References section of your project
    	//    It is not there by default
    	var acb = sender as System.Windows.Controls.AutoCompleteBox;
    	string txtComboText = acb.Text;
    
    	// We need to switch to the Logic Thread to check our combo text against the list of valid values
    	// TODO: You must compare your combo text against the property displayed by default in the drop down
    	//    I'm not sure how to automatically get that off hand, but I assume it is possible. 
    	this.Details.Dispatcher.BeginInvoke(() =>
    	{
    		if (this.ContactProperty.Country.CountryName != txtComboText) // Note: how the txtComboText variable is available to the dispatched thread
    		{
    			var country = this.DataWorkspace.ContactDataSource.Countrys.Where(gl => gl.CountryName == txtComboText).FirstOrDefault();
    			if (country == null) // text in combobox is not a valid selection
    			{
    				// We need to get back to the UI thread to access the Auto Complete Box and set its property
    				// otherwise we get a cross threading exception
    				Microsoft.LightSwitch.Threading.Dispatchers.Main.BeginInvoke(() =>
    				{
    					// NOTE: this example is using ContactProperty on a New Data Screen to get the current value
    					//    from a details screen you would probably use this.Contact.Country.CountryName
    					//    Obviously the exact names depend on your entities and how you name them.
    					acb.Text = this.ContactProperty.Country.CountryName; // Note again: how the acb variable is also available here.
    				});
    			}
    		}
    	});
    }
    
    

    Thanks to all who eventually helped me get to this solution...  Also check Tim Leung's post which answers the ComboBox databinding question.

     

     

     


    Rick
    • Marked as answer by RickWassum Monday, May 9, 2011 4:07 PM
    Monday, May 9, 2011 4:07 PM
  • Thanks Tejano,

    I believe I have downloaded those extensions earlier, but I have not had an opportunity to use them yet.  I'll have to take another look when I get a chance.

    As for whether you can somehow bind a combobox to an "auto" collection like the AutoCompleteBox, I still don't know, because I have no idea how the "auto" feature is implemented.

    Now that I can get the AutoCompleteBox to work as I want with a little work, it has the potential to be much more efficient than the ComboBox if implemented correctly, especially in the case of large lists with hundreds or thousands of possible values.  The ability to marshall a composable linq query all the way back to the server or even the database to retrieve a reduced set of values as opposed to retrieving the entire list makes the AutoCompleteBox much more flexible although the Modal Picker is still probably a better if you are returning hundreds of values, unless they really figure out a way to speed it up, possibly with caching, compression, and otherwise figuring out how to reduce some of the overhead that LightSwitch brings to the party.  Apparently the have some performance improvements planned for RTM, it is unfortunate and somewhat odd that we can't get some RC bits, but it is what it is.

     


    Rick
    Monday, May 9, 2011 4:43 PM