0

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>();
        }
    }
Clemens
  • 123,504
  • 12
  • 155
  • 268
nikhil
  • 1,578
  • 3
  • 23
  • 52
  • I just tried mocking your code in a test solution with awaits and IsBorderProgressRingVisible seems to be set as expected. I guess you are calling `Action ` in `MenuItem` through some event and guess that when it does not work for you. When you debug the code, does the finally clause get hit? Is it possible mock the `_atlasControlledModule` in your case and verify the functionality? – Vishwa Jul 02 '20 at 01:17
  • Yea the finally block hits for me. I see the break point setting the value to false for theIsBorderProgressRingVisible. I have BooleanToVisibilityConverter, The problem is even when the variable is set to false the break point comes into the converter. The _atlasControlledModule is just parsing some json data and returns an object. – nikhil Jul 02 '20 at 02:49
  • Could you show us the XAML code of how you binding `UserMaintenanceMethodsViewModel`? – Gellio Gao Jul 02 '20 at 03:50
  • Just edited the Post adding the xaml. – nikhil Jul 02 '20 at 04:09
  • 1
    Tangled code. Did you try `Binding` instead of `x:Bind`? Remove useless `.ConfigureAwait(true)`. Check, in what kind of thread the debugger hits the breadpont (combobox in VS toolbar). – aepot Jul 02 '20 at 06:18
  • Is it a UWP application not a WPF application, right? I tried these code in WPF and no problems. – Gellio Gao Jul 02 '20 at 06:28
  • 1
    How did you bind `IsBorderProgressRingVisible` and `RunMethodResult`, I tried the code in UWP and it all good as well. ps: the default `Mode` of `x:Bind` is `OneTime`, so make sure you use right mode for it. – Gellio Gao Jul 02 '20 at 07:09
  • Yes its a uwp app. Let me undo all the changes and try it from scratch. – nikhil Jul 02 '20 at 13:32
  • You need to update the property in current ui-thread. – Nico Zhu Jul 02 '20 at 14:11
  • Please try this `Window.Current.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal,() => { }); ` – Nico Zhu Jul 02 '20 at 14:13
  • This should work I think. Do you use a custom converter, if so could you post it? How many items do you generate? It's quite possible that the method return so fast that you won't see the visual feedback... Remove the the foreach and replace it with e.g `await Task.Delay(5000);` to see a busy indicator. – BionicCode Jul 02 '20 at 14:51
  • Here is the thing, I tried await Task.Delay(5000). Now in the code behind after using LifeTimeScope.Resolve the x:Bind dosen't work. The progress ring stays forever but like aepot says when I changed the same thing to Binding it works. Visibility="{x:Bind ViewModel.IsBorderProgressRingVisible,Mode=OneWay,Converter={StaticResource boolToVisibilityConverter}}" – nikhil Jul 02 '20 at 15:02
  • On the other hand if I say ViewModel=new UserMaintenanceViewModel(); instead of using LiteTimeScope.Resolve both x:Bind and Binding works. Thats strange. – nikhil Jul 02 '20 at 15:05
  • I don't understand why x:Bind won't work and Binding works when LifeTimeScope is used. – nikhil Jul 02 '20 at 16:01
  • What is LifeTimeScope? Looks like a Service Locator. It looks like it only works ()resolves) at runtime. `x:Bind` requires static compiletime references. – BionicCode Jul 02 '20 at 16:07
  • The LifeTimeScope is from the Autofac Dependency Injection framework. Ahh I see that might be why x:Bind is not working properly. But the problem is x:Bind is working fine from the other places in the project with LifeTimeScope. Only in this view its not. – nikhil Jul 02 '20 at 16:17
  • @aepot Can you help me with this post, https://stackoverflow.com/questions/65570025/getting-a-binding-path-exception-inside-of-a-data-template – nikhil Jan 04 '21 at 21:29

1 Answers1

2

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; }
BionicCode
  • 1
  • 4
  • 28
  • 44
  • can you help me with this https://stackoverflow.com/questions/64468864/displaymemberpath-is-not-working-on-combobox – nikhil Oct 21 '20 at 17:38