6

I'm implementing auto complete feature in my .NET MAUI app and I'm using CommunityToolkit.Mvvm code generators in my view model to handle observable properties.

I have the following code and I'm trying call GetSuggestions() method when the SearchText changes.

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(GetSuggestions))]
string searchText;

[ObservableProperty]
bool showSuggestions;

ObservableCollection<string> Suggestions { get; } = new();

private async Task GetSuggestions()
{
   if(string.IsNullOrEmpty(SearchText) || SearchText.Length < 3)
      return;

   var data = await _myApiService.GetSuggestions(SearchText.Trim());
   if(data != null && data.Count > 0)
   {
      Suggestions.Clear();
      foreach(var item in data)
         Suggestions.Add(item);

      ShowSuggestions = true;
   }
}

This is giving me the following error:

The target(s) of [NotifyCanExecuteChangedFor] must be an accessible IRelayCommand property, but "GetSuggestions" has no matches in type MyViewModel.

What am I doing wrong here?

Sam
  • 26,817
  • 58
  • 206
  • 383

2 Answers2

8

I guess there are two problems here.

Why is this error occurring?

That happens because GetSuggestions is not a Command. Try adding the [RelayCommand] attribute to your method.

[RelayCommand]
private async Task GetSuggestions()
{
    if(string.IsNullOrEmpty(SearchText) || SearchText.Length < 3)
       return;

    var data = await _myApiService.GetSuggestions(SearchText.Trim());
    if(data != null && data.Count > 0)
    {
        Suggestions.Clear();
        foreach(var item in data)
        Suggestions.Add(item);

        ShowSuggestions = true;
    }
}

Then link NotifyCanExecuteChangedFor to the autogenerated command.

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(GetSuggestionsCommand))]
string searchText;

The second one.

You need to

call GetSuggestions() method when the SearchText changes.

The NotifyCanExecuteChangedFor attribute doesn't do that.

In the autogenerated source code you should find an empty partial method called OnSearchTextPropertyChanged. Try implementing it.

partial void OnSearchTextPropertyChanged(string value)
{
    GetSuggestions();
}

If this is what you're searching for, GetSuggestions doesn't need to be marked with the RelayCommand attribute.

Riccardo Minato
  • 520
  • 2
  • 11
  • Thank you for the detailed explanation. Very useful indeed. A follow up question if I may: the `GetSuggestions()` is an `async` method. If I make it `partial void OnSearchTextPropertyChanged()` it works but it complains that I'm not awaiting `GetSugestions()`. It works fine though. Do I not need to `await` the call to `GetSuggestions()` method inside the `OnSearchTextPropertyChanged()` method? – Sam Jun 28 '22 at 03:08
  • Glad to hear that. You can mark the partial method as `async` so that you can await code inside its body. `partial async void OnSearchTextPropertyChanged(string value) { await GetSuggestions(); }`. It all depends on what you need to do. – Riccardo Minato Jun 28 '22 at 06:36
  • Actually, it doesn't like it when I add `async` to the method. When it's `partial async void OnSearchTextPropertyChanged(string value)`, it gives me an error. I also tried changing it to `partial async Task OnSearchTextPropertyChanged(string value)` and it doesn't like that either. – Sam Jun 29 '22 at 04:58
  • 1
    Sorry, my bad. I switched the keywords. It should work with `async partial void ...` – Riccardo Minato Jun 29 '22 at 06:28
  • While most of this was helpful, I need to do a few things different including using the `[RelayCommand]` and calling the method inside my OnPropChanged method to be `Task.Run(() => this.MyMethodAsync()).Wait();`. See my answer as more of a partial or ammendment to this one. – Adam Cox Nov 19 '22 at 06:54
0

Only meant as more of an amendment of @RMinato's answer.

As my comment say: "While most of this was helpful, I need to do a few things different including using the [RelayCommand] and calling the method inside my OnPropChanged method to be Task.Run(() => this.MyMethodAsync()).Wait();".

My code looks like:

[QueryProperty(nameof(Course), nameof(Course))]
public partial class CourseDetailViewModel : BaseViewModel
{
    private readonly CourseService courseService;

    public CourseDetailViewModel(CourseService courseService)
    {
        this.courseService = courseService;
    }

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(GetCourseDetailCommand))]
    Course course;

    partial void OnCourseChanged(Course value)
    {
        Task.Run(() => this.GetCourseDetailAsync()).Wait();
    }

    [RelayCommand]
    public async Task GetCourseDetailAsync()
    {
        if (GetCourseDetailCommand.IsRunning) return;

        try
        {
            IsBusy = true;

            course = await courseService.GetCourseDetailAsync(course.Id);
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Failed to get course detail. Error: {ex.Message}");
            await Shell.Current.DisplayAlert("Error!",
                $"Failed to get course detail: {ex.Message}", "OK");
            throw;
        }
        finally
        {
            IsBusy = false;
        }
    }
}
Adam Cox
  • 3,341
  • 1
  • 36
  • 46
  • Hi Adam. As I said in my answer, for the specific case or the author `NotifyCanExecuteChangedFor` wasn't really needed. I don't know if you're trying to define some more complex scenarios where you call `GetCourseDetailAsync` both as a command and as a normal method and change executability of it as a command. I find this kind of behavior weird, honestly. Anyway, are you sure you really need that `Task.Run(() => this.GetCourseDetailAsync()).Wait();`? The `Wait` method for a `Task` is something that's usually not wanted because it blocks everything occurring in current Task, possibly even UI – Riccardo Minato Nov 20 '22 at 17:46