1

ANSWERED BY @grek40

I have implemented INotifyPropertyChanged in my project and for everything else it is working fine, but for this one variable-bound textblock it is only updating once on the main window load event.

I'm probably just missing some little detail somewhere, please help!

MainWindow.xaml

<TextBlock HorizontalAlignment="Left" Margin="317,161,0,0"  
TextWrapping="Wrap" Text="{Binding ish.IsDoingWork, Mode=OneWay, 
UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top"/>

<Button  Command="{Binding hksVm.HentKundeStatus}" Content="Invoke" 
HorizontalAlignment="Left" Margin="704,139,0,0" VerticalAlignment="Top" 
Width="75"/>

MainWindow.xaml.cs (setting data context)

public MainWindow()    
{
    if (!ValidationHandler.GrantAccess().Equals(3))
    {
        InitializeComponent();
        DataContext = new
        {
            hksVm = new HentKundeStatusVm(),
            ish = new InvocationServiceHandler()
        };
    }
    else
    {
        Close();
    }
}

ViewModel.cs

using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Input;

namespace MyNamespace
{
    public class HentKundeStatusVm : IViewModel
    {
        private ICommand _hentKundeStatus;
        private readonly InvocationServiceHandler _invocationServiceHandler = new InvocationServiceHandler();


        public ICommand HentKundeStatus => HentKundeStatusCommand();

        public ICommand HentKundeStatusCommand()
        {
            if (ValidationHandler.GrantAccess() < 2)
            {
                return _hentKundeStatus ?? (_hentKundeStatus = new RelayCommand(param =>
                           ElapsedTime = _invocationServiceHandler.ExecuteAndTimeAction(
                               () =>
                               {
                                   //web API kaldes asynkront - husk: using System.Net.Http; 
                                   using (var client = new HttpClient().GetAsync("API-url"))
                                   {
                                       client.Result.Content.ReadAsStringAsync();
                                   }
                               }, AntalKald)));
            }
            return null;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public void NotifyPropertyChanged(string property)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
        }
    }
}

InvocationServiceHandler.cs

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading.Tasks;
using App005_WebServiceTestingTool_Domain.Annotations;

namespace App005_WebServiceTestingTool_Domain.Handlers
{
    public class InvocationServiceHandler : INotifyPropertyChanged
    {
        // This works on the main_window load event and set the textblock in the view
        private string _isDoingWork = "Currently not working";
        public string IsDoingWork
        {
            get => _isDoingWork;
            set
            {
                _isDoingWork = value;
                NotifyPropertyChanged(nameof(IsDoingWork));
            }
        }

        /// <summary>
        /// Method that invokes action parameter x times in multiple threads (parallel) and returns the elapsed time
        /// </summary>
        /// <param name="action"></param>
        /// <param name="antalKald"></param>
        /// <returns></returns>
        public string ExecuteAndTimeAction(Action action, string antalKald)
        {
            // Here is set the bound variable, and if I debug I can see it getting set to Working...
            IsDoingWork = "Working...";
            var sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < Convert.ToInt32(antalKald); i++)
            {
               action.Invoke();
            }
            sw.Stop();
            // Here I am resetting the variable and again in debug I can see it change, but nothing happens in the view
            IsDoingWork = "";
            return $"Elapsed time: {sw.Elapsed}";
        }

        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        public void NotifyPropertyChanged(string property)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
        }
    }
}
SirTaphos
  • 13
  • 6
  • 2
    The UI is only updated when you free the UI Thread to do so. It appears you keep blocking the UI thread while executing your action? – grek40 Jun 15 '18 at 06:14
  • See also https://stackoverflow.com/questions/28253140/parallel-for-not-to-use-my-main-thread – grek40 Jun 15 '18 at 06:16
  • Wouldn't the textblock then update after the action has run? – SirTaphos Jun 15 '18 at 06:17
  • 1
    How do you run `ExecuteAndTimeAction`? Please show a [mcve] as in complete (more details) and minimal (less unneeded things like the combination of Binding `Mode=OneWay, UpdateSourceTrigger=PropertyChanged` it's never going to trigger updates with OneWay) – grek40 Jun 15 '18 at 06:20
  • @grek40 - OneWay is OneWayToTarget so that part looks Ok. – bommelding Jun 15 '18 at 06:28
  • 'Currently not working' is the only thing it says, and it retrieves that from the bound variable, so I assume that the binding works. @bommelding – SirTaphos Jun 15 '18 at 06:39

2 Answers2

0

Your logic is fine , everything looks properly attached and the values are updated as expected . The only problem is your UI isn't updating it because you're executing the For loop on the main thread which unfortunately is blocking all your UI updates.

So You can

  1. Run ExecuteAndTimeAction as a background operation using, Backgroundworker/[Task Library][1].

2.Use Dispatcher to flush your UI messages, eg:

    /// <summary>
    /// Enters the message loop to process all pending messages down to the specified
    /// priority. This method returns after all messages have been processed.
    /// </summary>
    /// <param name="priority">Minimum priority of the messages to process.</param>
    public static void DoEvents(DispatcherPriority priority = DispatcherPriority.Background)
    {
        DispatcherFrame frame = new DispatcherFrame();
        Dispatcher.CurrentDispatcher.BeginInvoke(
            priority,
            new DispatcherOperationCallback(ExitFrame), frame);
        Dispatcher.PushFrame(frame);
    }
    private static object ExitFrame(object f)
    {
        ((DispatcherFrame)f).Continue = false;
        return null;
    }

And Call

        /// <summary>
        /// Method that invokes action parameter x times in multiple threads (parallel) and returns the elapsed time
        /// </summary>
        /// <param name="action"></param>
        /// <param name="antalKald"></param>
        /// <returns></returns>
        public string ExecuteAndTimeAction(Action action, string antalKald)
        {
            // Here is set the bound variable, and if I debug I can see it getting set to Working...
            IsDoingWork = "Working...";
            DoEvent();//flushes the UI msg queue
            var sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < Convert.ToInt32(antalKald); i++)
            {
               action.Invoke();
            }
            sw.Stop();
            // Here I am resetting the variable and again in debug I can see it change, but nothing happens in the view
            IsDoingWork = "";
            DoEvent();//flushes the UI msg queue
            return $"Elapsed time: {sw.Elapsed}";
        }

The second approach is a hack and it will still freeze the UI but will get the job done for you. I suggest you go for the first approach, It is way better but takes effort to implement.

Dharani Kumar
  • 457
  • 2
  • 8
0

Basically, you have two instances of InvocationServiceHandler. One inside the DataContext as ish = new InvocationServiceHandler() and the other inside MyViewModel as private readonly InvocationServiceHandler _invocationServiceHandler = new InvocationServiceHandler();

So ish.IsDoingWork is displayed and something like hksVm._invocationServiceHandler.IsDoingWork is updated. It's not really clear since HentKundeStatusVm and MyViewModel are not really the same thing in the question.

It should be possible to fix this situation by Constructor Injection of the service handler:

public class HentKundeStatusVm : IViewModel
{
    private readonly InvocationServiceHandler _invocationServiceHandler;

    public HentKundeStatusVm(InvocationServiceHandler ish)
    {
        _invocationServiceHandler = ish;
    }

    // the other stuff
}

Then

// In the MainWindow constructor
var ishInstance = new InvocationServiceHandler();
DataContext = new
{
    hksVm = new HentKundeStatusVm(ishInstance),
    ish = ishInstance
};

Then you have the same handler instance available for binding and for execution.

grek40
  • 13,113
  • 1
  • 24
  • 50
  • Hi @grek40, thank you for your reply. I have edited my question, so that MyViewModel is renamed back to HentKundeStatusVm, the edit to 'MyViewModel' was an error, I apologize. As for the instances, I have to bind my view directly to InvocationServiceHandler since I would get a recursion error if I were to update the same property in my HentKundestatusVm from InvocationServiceHandler. Could this be the issue you think? – SirTaphos Jun 18 '18 at 05:55
  • @BjørnFrierPedersen honestly I don't understand the part about recursion error - it sounds like something that happens when you write different code (but I can't see that code in the question). Anyway, as answered, you need to have only one instance of `InvocationServiceHandler` and you need to bind to this instance and also use it to invoke your `ExecuteAndTimeAction`. Currently, you do these two things with two distinct instances of the handler. Do you have any problems in reducing your number of handler instances? – grek40 Jun 18 '18 at 06:16
  • The HentKundeStatusVm/vm.cs invokes ExecuteAndTimeAction() from InvocationServiceHandler.cs/ish.cs and the IsDoingWork variable is bound from the ish.cs to the view. But if I were to move the IsDoingWork variable from ish.cs to vm.cs and bind it from there, creating only one handler instance of ish.cs, the vm.cs would call ExecuteAndTimeAction and update itself, creating the recursion error. That is why I tried to do it like this. But if this creates so many problems with the bindings, perhaps I should rethink the entire structur of my application. – SirTaphos Jun 18 '18 at 06:25
  • @BjørnFrierPedersen don't make it complicated... see my edit. – grek40 Jun 18 '18 at 06:43
  • I was not trying to over-complicate, just trying to answer your previous question. Your edit looks good, I'll try to implement it as soon as my Visual Studio installation is working again. – SirTaphos Jun 18 '18 at 06:49