Property changed is not updating the UI from inside a task

北慕城南 提交于 2020-07-04 04:19:28

问题


Firstly I have a user control which has a dependency property as follows. The MenuItems property is bound to some List control on the UI.

public static readonly DependencyProperty MenuItemsProperty = DependencyProperty.Register(
    nameof(MenuItems),
    typeof(IEnumerable<MenuItem>),
    typeof(MenuViewControl),
    new PropertyMetadata(null));

public IEnumerable<MenuItem> MenuItems
{
    get => (IEnumerable<MenuItem>)GetValue(MenuItemsProperty);
    set => SetValue(MenuItemsProperty, value);
}

The MenuItem class is as follows which has 3 properties,

public class MenuItem : BindableBase
{
        private string _text;
        private Action _action;
        private ICommand _executeCommand;

         public string Text
        {
            get => _text;
            set => Set(ref _text, value);
        }
     
        public Action Action
        {
            get => _action;
            set => Set(ref _action, value);
        }

        public ICommand ExecuteCommand
        {
            get => _executeCommand ?? (_executeCommand = new RelayCommand(Action, _canExecute));
            set
            {
                if (Set(ref _executeCommand, value))
                {
                    CanExecute = () => _executeCommand?.CanExecute(null) ?? true;
                    _executeCommand.CanExecuteChanged += (sender, args) => RaisePropertyChanged(nameof(IsEnabled));
                }
            }
        }
}

Now somewhere in my code I want to reuse the above user control. Along the same lines I need to call some async methods. So I have a view model class for the current UI where I will be calling the above user control as follows. My problem is the IsBorderProgressRingVisible is never being set to false and the RunMethodResult never updates the TextBlock in the current UI. Please help.

    public class UserMaintenanceMethodsViewModel:BindableBase
    {
    //This collection is bound to the above UserControl's MenuItem property on my current UI.
     private ObservableCollection<MenuItem> _userMaintenanceMenuCollection;
     public ObservableCollection<MenuItem> UserMaintenanceMenuCollection
            {
                get => _userMaintenanceMenuCollection;
                set => Set(ref _userMaintenanceMenuCollection, value);
            }
    
    //This string is bound to a textblock
     private string _runMethodResult;
     public string RunMethodResult
            {
                get => _runMethodResult;
                set => Set(ref _runMethodResult, value);
            }

  //This property is bound to a progress ring.
     private bool _isBorderProgressRingVisible;
      public bool IsBorderProgressRingVisible
        {
            get => _isBorderProgressRingVisible;
            set => Set(ref _isBorderProgressRingVisible, value);
        }
    //In my constructor I am calling some async methods as follows..
        public UserMaintenanceMethodsViewModel()
        {
                    _ = PopulateServiceMethods();
        
        }
        
               //Problem in this method is once the IsBorderProgressRingVisible is set to true, it never sets the value back to false. As a result the progress ring never collapses.
        //The other problem is the RunMethodResult which is bound to a textblock never gets updated. Please help.
                private async Task PopulateServiceMethods()
                {
                    try
                    {
                        if (_atlasControlledModule != null)
                        {
                            IsBorderProgressRingVisible = true;
                            UserMaintenanceMenuCollection = new ObservableCollection<MenuItem>();
                            var Methods = await _atlasControlledModule.GetServiceMethods(AtlasMethodType.Maintenance).ConfigureAwait(true);
        
                            foreach (var method in Methods)
                            {
                                UserMaintenanceMenuCollection.Add(new MenuItem()
                                {
                                    Text = method.Name,
                                    Action = async () =>
                                   {
                                        var result = await ExcuteAtlasMethod(method).ConfigureAwait(true);
                                       RunMethodResult = result.Status.ToString(); //The textblock on the UI never gets updated.
                                   },
                                    Warning = false
                                });
                            }
                        }
                    }
                    finally
                    {
                        IsBorderProgressRingVisible = false; //This code dosen't work.
                    }
                }
        
                  private async Task<AtlasMethodRequest> ExcuteAtlasMethod(AtlasMethod method)
                {
                    try
                    {
                        IsBorderProgressRingVisible = true;
                        return await _atlasControlledModule.CallMethod(method);
                    }
                    finally
                    {
        
                        IsBorderProgressRingVisible = false;
                    }
                }
        }

Edit: Here is the Xaml for the current view

<viewCommon:PageViewBase
    x:Class="Presentation.InstrumentUI.ViewsLoggedIn.UserMaintenanceMethodsView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:viewCommon="using:Presentation.InstrumentUI.Common"
    xmlns:viewsCommon="using:Presentation.InstrumentUI.ViewsCommon"
    xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
    xmlns:core="using:Microsoft.Xaml.Interactions.Core"
    xmlns:valueConverters="using:Presentation.Common.ValueConverters"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">
<Grid>
 <viewsCommon:MenuViewControl x:Name="UserMaintenanceMethodsMenuView"
                                         Grid.Row="0"
                                         Title="{Binding UserMaintenanceMethodsTitle, Source={StaticResource StringResources}}"
                                         LifetimeScope="{x:Bind LifetimeScope}"
                                         MenuItems="{x:Bind ViewModel.UserMaintenanceMenuCollection,Mode=OneWay}"
                                         HeaderVisibility="Visible">
            </viewsCommon:MenuViewControl>
</Grid>
</viewCommon:PageViewBase>

This is the xaml.cs

public sealed partial class UserMaintenanceMethodsView : PageViewBase
    {
        public IUserMaintenanceMethodsViewModel ViewModel { get; set; }
        public UserMaintenanceMethodsView()
        {
            this.InitializeComponent();
            ViewModel = LifetimeScope.Resolve<IUserMaintenanceMethodsViewModel>();
        }
    }

回答1:


From what I see, you code should generally work. The problem is that all your code is executed in the constructor of UserMaintenanceMethodsViewModel. You shouldn't call long running methods from a constructor and you shouldn't call async methods from your constructor. An asynchronous method generally indicates some long running or CPU heavy operation. It should be moved outside the constructor so that you can execute it asynchronously.

Also the way you invoke the asynchronous method from the constructor is wrong:

ctor()
{
  // Executes synchronously. 
  // Leaves object in undefined state as the constructor will return immediately.
  _ = PopulateServiceMethodsAsync();
}

The previous example will execute the method PopulateServiceMethods synchronously. Furthermore, the constructor will return before the method has completed, leaving the instance in an uninitialized state.
The caller of the constructor will continue and probably use the instance, assuming that it is ready to use. This might lead to unexpected behavior.

To solve this, you should move the resource intensive initialization to a separate method:

ctor()
{
  // Some instance member initialization
}

// Call later e.g. on first access of property internally or externally
public async Task InitializeAsync()
{
  // Some CPU heavy or long running initialization routine
}

You can also consider to instantiate this type deferred using Lazy<T> or AsyncLazy<T>.


This property in MenuItem class has a "dangerous" setter:

public ICommand ExecuteCommand
{
  get => _executeCommand ?? (_executeCommand = new RelayCommand(Action, _canExecute));
  set
  {
    if (Set(ref _executeCommand, value))
    {
      CanExecute = () => _executeCommand?.CanExecute(null) ?? true;
      _executeCommand.CanExecuteChanged += (sender, args) => RaisePropertyChanged(nameof(IsEnabled));
    }
  }
}

Calling the set method will replace the previous command without unsubscribing from the old CanExecuteChanged event. This can lead to memory leaks in certain scenarios. Always unsubscribe from the old instance before subscribing to the new instance.
Also I'm not quite sure why you are listening to this event at all. Usually controls listen to this event in. E.g., a Button subscribes to this event and when it is raised, it would invoke ICommand.CanExecute again to deactivate itself, if this method returns false. From your view model you usually want to call RaiseCanExecuteChanged on your command to trigger re-evaluation for all controls (or implementations of ICommandSource).


Using async in lambdas can also lead to unexpected behavior:

Action = async () =>
  {
    var result = await ExcuteAtlasMethod(method).ConfigureAwait(true);
    RunMethodResult = result.Status.ToString(); // The textblock on the UI never gets updated.
  }

Executing the Action won't cause the thread to wait asynchronously, because the delegate is not awaited. Execution continues. You should consider to implement the RelayCommand that it accepts a Func<object, Task>. This way the invocation of the delegate can be awaited.


{x:Bind} has a different behavior than {Binding}. x:Bind is a compiletime binding. It doesn't bind to the DataContext and requires a static binding source. You should debug your code in order to check if LifeTimeScope resolves properly. Maybe it executes on a different thread. You can try to change ViewModel to a DependencyProperty.

I also realized that you are binding to a property that is declared as interface type:

public IUserMaintenanceMethodsViewModel ViewModel { get; set; }

This won't work. Please try to replace the interface with a concrete type. I think this will solve this. E.g.,

public UserMaintenanceMethodsViewModel ViewModel { get; set; }


来源:https://stackoverflow.com/questions/62687039/property-changed-is-not-updating-the-ui-from-inside-a-task

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!