Call method when ObservableProperty changes using CommunityToolkit.Mvvm
Asked Answered
S

3

10

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?

Spermatic answered 25/6, 2022 at 23:42 Comment(0)
L
15

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.

Laban answered 27/6, 2022 at 7:19 Comment(5)
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?Spermatic
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.Laban
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.Spermatic
Sorry, my bad. I switched the keywords. It should work with async partial void ...Laban
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.Stoichiometric
S
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;
        }
    }
}
Stoichiometric answered 19/11, 2022 at 7:0 Comment(1)
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 UILaban
Z
0

For a more recent Maui solution, using the community toolkit, each observable property has an equivalent partial On-PropertyName-Changed event. Simply state what this should be and you're away.

Example in this SO answer

Zephan answered 29/8 at 13:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.