【问题标题】:Property changed is not updating the UI from inside a task属性更改不会从任务内部更新 UI
【发布时间】:2020-10-22 12:05:07
【问题描述】:

首先,我有一个具有如下依赖属性的用户控件。 MenuItems 属性绑定到 UI 上的某个 List 控件。

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);
}

MenuItem 类如下,它有 3 个属性,

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));
                }
            }
        }
}

现在我想在我的代码中的某个地方重用上面的用户控件。同样,我需要调用一些异步方法。所以我有一个当前 UI 的视图模型类,我将在其中调用上述用户控件,如下所示。我的问题是 IsBorderProgressRingVisible 永远不会设置为 false 并且 RunMethodResult 永远不会更新当前 UI 中的 TextBlock。请帮忙。

    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;
                    }
                }
        }

编辑:这是当前视图的 Xaml

<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>

这是 xaml.cs

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

【问题讨论】:

  • 我刚刚尝试在带有等待的测试解决方案中模拟您的代码,并且 IsBorderProgressRingVisible 似乎按预期设置。我猜你是通过一些事件在MenuItem 中调用Action ,并猜测它什么时候对你不起作用。当你调试代码时,finally 子句会被命中吗?是否可以在您的情况下模拟 _atlasControlledModule 并验证功能?
  • 是的,最终块对我来说很重要。我看到断点将 IsBorderProgressRingVisible 的值设置为 false。我有 BooleanToVisibilityConverter,问题是即使变量设置为 false,断点也会进入转换器。 _atlasControlledModule 只是解析一些 json 数据并返回一个对象。
  • 你能告诉我们你如何绑定UserMaintenanceMethodsViewModel的XAML代码吗?
  • 纠结的代码。你试过Binding而不是x:Bind吗?删除无用的.ConfigureAwait(true)。检查,调试器在哪种线程中触发了面包点(VS 工具栏中的组合框)。
  • 你是如何绑定IsBorderProgressRingVisible 和RunMethodResult 的,我在UWP 中尝试了代码,一切都很好。 ps:x:Bind的默认Mode是OneTime,所以一定要使用正确的模式。

标签: c# xaml uwp async-await


【解决方案1】:

据我所知,您的代码通常应该可以工作。问题是你所有的代码都是在UserMaintenanceMethodsViewModel的构造函数中执行的。您不应从构造函数调用长时间运行的方法,也不应从构造函数调用异步方法。异步方法通常表示一些长时间运行或 CPU 繁重的操作。应该将它移到构造函数之外,以便您可以异步执行它。

你从构造函数调用异步方法的方式也是错误的:

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

前面的例子将同步执行方法PopulateServiceMethods。此外,构造函数将在方法完成之前返回,使实例处于未初始化状态。
构造函数的调用者将继续并可能使用该实例,假设它已准备好使用。这可能会导致意外行为。

要解决这个问题,您应该将资源密集型初始化移到单独的方法中:

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
}

您还可以考虑使用Lazy&lt;T&gt; 或AsyncLazy&lt;T&gt; 延迟实例化这种类型。


MenuItem 类中的这个属性有一个“危险”的设置器:

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));
    }
  }
}

调用 set 方法将替换之前的命令,而不会取消订阅旧的 CanExecuteChanged 事件。在某些情况下,这可能会导致内存泄漏。在订阅新实例之前,请始终取消订阅旧实例。
另外,我不太确定您为什么要收听此活动。通常控件会在其中监听此事件。例如,Button 订阅此事件,当它被引发时,如果此方法返回 false,它将再次调用 ICommand.CanExecute 以停用自身。在您的视图模型中,您通常希望在命令上调用 RaiseCanExecuteChanged 以触发对所有控件(或 ICommandSource 的实现)的重新评估。


在 lambda 中使用 async 也会导致意外行为:

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

执行Action 不会导致线程异步等待,因为不等待委托。执行继续。您应该考虑实现接受Func&lt;object, Task&gt; 的RelayCommand。这样就可以等待委托的调用。


{x:Bind} 的行为与{Binding} 不同。 x:Bind 是编译时绑定。它不绑定到 DataContext 并且需要静态绑定源。您应该调试代码以检查 LifeTimeScope 是否正确解析。也许它在不同的线程上执行。您可以尝试将ViewModel 更改为DependencyProperty。

我还意识到您正在绑定到一个声明为接口类型的属性:

public IUserMaintenanceMethodsViewModel ViewModel { get; set; }

这行不通。请尝试用具体类型替换接口。我认为这将解决这个问题。例如,

public UserMaintenanceMethodsViewModel ViewModel { get; set; }

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2020-02-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-04-06
  • 1970-01-01
相关资源
最近更新 更多