Creating new child entities through AutoCompleteBox in LightSwitch
Recently I had a visit from my brother regarding a programming question in LightSwitch. He was stuck on some functionality he wanted to implement, more precisely the ability to add a new record through an AutoCompleteBox if the record was nonexistent.
After playing around with LightSwitch for a while we had to conclude that this was not available out-of-the-box and it had to be implemented manually. I told him I would solve the problem during the Christmas holidays.
Not thinking about his problem I continued working on a LightSwitch application regarding dogs and their breeders, and as you might expect I ran into the same requirement after a few screens.
In my situation I had an entity called Breeder, which is linked to an entity called Person. While adding a new Breeder record, I wanted to assign the Person as well. No problem if it exists, but end of the road if it doesn’t. I fancied a dialog box asking me to add the missing record and presenting a modal popup to fill the required fields, after which the records would be added and used.
After consulting Google, I found a link to Tim Leung’s blog
That was pretty much what I needed, but after implementing the solution some issues remained. I was not very happy with the database roundtrip needed to see if the search criteria would result in a valid record, I prefer to use the enter key to submit the criteria instead of tabbing to the next control and triggering a focus change and last but not least I wanted a modal dialog to allow all required fields to be added.
Debugging the solution proposed by Tim revealed that besides the Text property, the System.Windows.Controls.AutoCompleteBox has another property called SearchText. This property always contains the search criteria entered while the Text property contains the text from the located record or is empty in case nothing is found.
That was exactly what I was looking for, so I changed my code to:
partial void BreederDetail_Created() { // Write your code here. this.FindControl("Person").ControlAvailable += PersonControlAvailable; } void PersonControlAvailable(object sender, ControlAvailableEventArgs e) { var control = ((System.Windows.Controls.Control)e.Control); control.LostFocus += PersonLostFocus; } void PersonLostFocus(object sender, System.Windows.RoutedEventArgs e) { PopupIfNeeded(sender); } private void PopupIfNeeded(object sender) { var autoCompleteBox = ((System.Windows.Controls.AutoCompleteBox)sender); var value = autoCompleteBox.SearchText; var hasValue = !string.IsNullOrEmpty(autoCompleteBox.Text); Details.Dispatcher.BeginInvoke(delegate { if (string.IsNullOrEmpty(value)) return; if (!hasValue) { if ( this.ShowMessageBox( string.Format("Do you want to add the person {0}?", value), "Add Person", MessageBoxOption.YesNo) == System.Windows.MessageBoxResult.Yes) { var selectedPerson = DataWorkspace.PedigreeData.People.AddNew(); selectedPerson.Surname = value; Breeder.Person = selectedPerson; } } }); }
This solved the problem with the additional roundtrip to the database. Be sure not to access the AutoCompleteBox from within the Dispatcher call, this will result in a thread access error because the control was created by another thread.
The next thing to tackle was creating the record by pressing the enter key, while most of the plumbing was already in place, I simply added a KeyUp handler to intercept the Enter key.
void PersonControlAvailable(object sender, ControlAvailableEventArgs e) { var control = ((System.Windows.Controls.Control)e.Control); control.LostFocus += PersonLostFocus; control.KeyUp += ControlKeyUp; } void ControlKeyUp(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) { PopupIfNeeded(sender); e.Handled = true; } }
One problem I now encountered was duplicate calls to PopupIfNeeded because the Enter resulted in a focus change which in turn resulted in a second call to the PopupIfNeeded method. I decided to add a Boolean to see if the popup was already triggered.
private bool _popupActive; void PersonLostFocus(object sender, System.Windows.RoutedEventArgs e) { if (!_popupActive) PopupIfNeeded(sender); } private void PopupIfNeeded(object sender) { var autoCompleteBox = ((System.Windows.Controls.AutoCompleteBox)sender); var value = autoCompleteBox.SearchText; var hasValue = !string.IsNullOrEmpty(autoCompleteBox.Text); Details.Dispatcher.BeginInvoke(delegate { if (_popupActive) return; _popupActive = true; try { if (string.IsNullOrEmpty(value)) return; if (!hasValue) { if ( this.ShowMessageBox( string.Format("Do you want to add the person {0}?", value), "Add Person", MessageBoxOption.YesNo) == System.Windows.MessageBoxResult.Yes) { var selectedPerson = DataWorkspace.PedigreeData.People.AddNew(); selectedPerson.Surname = value; Breeder.Person = selectedPerson; } } } finally { _popupActive = false; } }); }
Last but not least I had to devise a way to show the Person record in a modal window and allow the properties to be set.
As it turns out, you can show and hide modal windows through calls to this.OpenModalWindow and this.CloseModalWindow on the screen instance. So the next thing was how to add a modal window to my existing BreederDetails screen.
A quick Google resulted in the following page:
Where the “Using a Modal Window” section is of particular interest.
Following the recipe, it gave me a nice modal window called “AddPersonDetails”
In the Command Bar I added two buttons, one to cancel the change and one to approve it. For the OK button I created a new method called “AddPersonDetailsOk”. As the process is a bit tideous, I’ll describe it.
Execute the Add Button command and change the Name in New Method to a name you can easily distinguish in the backend code.
Press OK and go to the properties on the lower right side of Visual Studio
Change the Display Name of the button into what you want the button to show.
Next, go to the left side of visual studio and locate the placeholder for the “AddPersonalDetailsOk” method you just created. Select it.
Again, in the properties on the right, click on the “Edit Execute Code” to go to the event handler for the method.
Add the call to CloseModalWindow to close the modal windowl, the entity is already updated and a save of the screen will persist it to the database.
partial void AddPersonDetailsOk_Execute() { // Write your code here. this.CloseModalWindow("AddPersonDetails"); }
I repeated the steps for a second button with the name “AddPersonDetailsCancel” and edited the execute code to match the following:
partial void AddPersonDetailsCancel_Execute() { // Write your code here. this.CloseModalWindow("AddPersonDetails"); // Get the newly created instance var person = Breeder.Person; // Delete the refetence on the breeder Breeder.Person = null; // Dispose of the newly created item so it is not saved person.Delete(); }
The general idea is to delete the newly created instance when a cancel is requested. To show the Modal Window I changed the Dispatcher.Invoke code to include a call to this.OpenModalWindow:
if (!hasValue) { if (this.ShowMessageBox( string.Format("Do you want to add the person {0}?", value), "Add Person", MessageBoxOption.YesNo) == System.Windows.MessageBoxResult.Yes) { // … this.OpenModalWindow("AddPersonDetails"); } }
It is good to see that there is a nice balance between the LightSwitch designer and the code. If you want something special it can be done without too much of a fuss, if you know where to look and Google 😉
Somewhere is the future I will wrap things in a neat reusable component, but for now enjoy.