mvvmsilverlight-4.0silverlight-toolkit

How can I marry AutoCompleteBox.PopulateComplete method with the MVVM paradigm?


Here is the setup: I have an autocompletebox that is being populated by the viewmodel which gets data from a WCF service. So it's quite straightforward and simple so far.

Now, I am trying to follow the principles of MVVM by which the viewmodel doesn't know anything about the view itself. Which is good, because I bound the Populating event of the autocomplete box to a method of my viewmodel via triggers and commands.

So the view model is working on fetching the data, while the view is waiting. No problems yet.

Now, the view model got the data, and I passed the collection of results to a property bound to the ItemSource property of the control. Nothing happens on the screen.

I go to MSDN and to find the officially approved way on how this situation is supposed to be handled (http://msdn.microsoft.com/en-us/library/system.windows.controls.autocompletebox.populating(v=vs.95).aspx):

  • Set the MinimumPrefixLength and MinimumPopulateDelay properties to values larger than the default to minimize calls to the Web service.

  • Handle the Populating event and set the PopulatingEventArgs.Cancel property to true.

  • Do the necessary processing and set the ItemsSource property to the desired item collection.

  • Call the PopulateComplete method to signal the AutoCompleteBox to show the drop-down.

Now I see a big problem with the last step because I don't know how I can call a method on a view from the view model, provided they don't know (and are not supposed to know!) anything about each other.

So how on earth am I supposed to get that PopulateComplete method of view called from the view model without breaking MVVM principles?


Solution

  • If you use Blend's Interactivity library, one option is an attached Behavior<T> for the AutoCompleteBox:

    public class AsyncAutoCompleteBehavior : Behavior<AutoCompleteBox>
    {
        public static readonly DependencyProperty SearchCommandProperty
            = DependencyProperty.Register("SearchCommand", typeof(ICommand), 
                  typeof(AsyncAutoCompleteBehavior), new PropertyMetadata(null));
    
        public ICommand SearchCommand
        {
            get { return (ICommand)this.GetValue(SearchCommandProperty); }
            set { this.SetValue(SearchCommandProperty, value); }
        }
    
        protected override void OnAttached()
        {
            this.AssociatedObject.Populating += this.PopulatingHook;
        }
    
        protected override void OnDetaching()
        {
            this.AssociatedObject.Populating -= this.PopulatingHook;
        }
    
        private void PopulatingHook(object sender, PopulatingEventArgs e)
        {
            var command = this.SearchCommand;
            var parameter = new SearchCommandParameter(
                    () => this.AssociatedObject
                              .Dispatcher
                              .BeginInvoke(this.AssociatedObject.PopulateComplete),
                    e.Parameter);
            if (command != null && command.CanExecute(parameter))
            {
                // Cancel the pop-up, execute our command which calls
                // parameter.Complete when it finishes
                e.Cancel = true;
                this.SearchCommand.Execute(parameter);
            }
        }
    }
    

    Using the following parameter class:

    public class SearchCommandParameter
    {
        public Action Complete
        {
           get;
           private set;
        }
    
        public string SearchText
        {
           get;
           private set;
        }
    
        public SearchCommandParameter(Action complete, string text)
        {
            this.Complete = complete;
            this.SearchText = text;
        }
    }
    

    At this point you need to do 2 things:

    1. Wire up the Behavior

      <sdk:AutoCompleteBox MinimumPopulateDelay="250" MinimumPrefixLength="2" FilterMode="None">
          <i:Interaction.Behaviors>
              <b:AsyncAutoCompleteBehavior SearchCommand="{Binding Search}" />
          </i:Interaction.Behaviors>
      </sdk:AutoCompleteBox>
      
    2. Create a DelegateCommand which handles your aysnc searching.

      public class MyViewModel : ViewModelBase
      {
          public ICommand Search
          {
              get;
              private set;
          }
      
          private void InitializeCommands()
          {
              this.Search = new DelegateCommand<SearchCommandParamater>(DoSearch);
          }
      
          private void DoSearch(SearchCommandParameter parameter)
          {
              var client = new WebClient();
              var uri = new Uri(
                  @"http://www.example.com/?q="
                  + HttpUtility.UrlEncode(parameter.SearchText));
              client.DownloadStringCompleted += Downloaded;
              client.DownloadStringAsync(uri, parameter);
          }
      
          private void Downloaded(object sender, DownloadStringCompletedEventArgs e)
          {
              // Do Something with 'e.Result'
              ((SearchCommandParameter)e.UserState).Complete();
          }
      }