4

I want to update my UI, but it didn't get updated, even when the PropertyChanged event got called. At startup i get the first values, but after changes it won't update.

While Debugging, i can see, that the values getting updated and the PropertyChanged event got fired, but the getter didn't get called.

(BTW: I'm relatively new to C#, respectively to the M-V-VM concept.)

XAML

<Page.DataContext>
    <vm:MainPageViewModel x:Name="ViewModel" />
</Page.DataContext>
...
<TextBlock x:Name="LatitudeVar" Text="{Binding LatitudeVar, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="79" Canvas.Left="189" Canvas.Top="38" />

C# (MainPageViewModel)

public class MainPageViewModel : ViewModelBase, INotifyPropertyChanged
{
    private Session _session;
    public MainPageViewModel()
    {
        _session = Session.Instance;
    }

    public static readonly MainPageViewModel Instance = new MainPageViewModel();

    public override async Task OnNavigatedToAsync(object parameter, NavigationMode mode, IDictionary<string, object> suspensionState)
    {

        // App just started, so we get GPS access and eventually initialize the client
        Session.InitializeClient();
        await StartGpsDataService();
        await Task.CompletedTask;
    }

    ...

    private string _latitude;
    public string LatitudeVar
    {
        get { return _session.Latitude; }
        set { _session.Latitude = value; NotifyPropertyChanged(); }
    }

    ...

    public async Task StartGpsDataService()
    {
        await Session.InitializeDataUpdate();
    }

     public new event PropertyChangedEventHandler PropertyChanged;

    [Annotations.NotifyPropertyChangedInvocator]
    protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

C# (Session)

public class Session : INotifyPropertyChanged
{
    public static void InitializeClient()
    {
        MainPageViewModel mpvm = new MainPageViewModel();
        _mpvm = MainPageViewModel.Instance;
    }

    private static Session _instance;
    public static Session Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new Session();
            }
            return _instance;
        }
    }


    private static Session _sc;
    internal static Session Sc
    {
        get { return _sc; }
        set { _sc = value; }
    }


    private static MainPageViewModel _mpvm;

    private string _latitude;
    public string Latitude
    {
        get { return _latitude; }
        set
        {
            if (_latitude == value) return; _latitude = value; RaiseOnPropertyChanged("Latitude"); }
    }

    ...

     public void UpdateGpsData(Geopoint point, Geopoint geopointOld)
    {
        _mpvm.LatitudeVar = point.Position.Latitude.ToString();
    }

    public static async Task InitializeDataUpdate()
    {
       Sc = Session.Instance;
       Sc.StartTime = DateTime.Now;
       Sc.GetPosition(Geoposition.Coordinate.Point);
    }

    public new event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void RaiseOnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

Some ways i tried before:

C# (MainPageViewModel)

private string _latitude;
public string LatitudeVar
{
    get { return _latitude; }
    set { _latitude = value; NotifyPropertyChanged("LatitudeVar"); }
}

Result: Values were not displayed.

C# (Session)

public void UpdateGpsData(Geopoint point, Geopoint geopointOld)
{
    Latitude = point.Position.Latitude.ToString();
}

Result: Values were diplayed at startup, but not updated.

EDIT: Found a solution: Some code sorting, thanks for the hint ;) Now my Session.cs is a Model. All relavant methods are now in the ViewModel. And i took care of, that only one instance exists:

public MainPageViewModel()
{
   _session = Session.Instance;
   _instance = this;
}
  • It's normal to send the property name like you did with "LatitudeVar", when you say values were not displayed - was there any binding errors? (Did you update your binding to point to LatitudeVar instead of Latitude?) I've never seen property change notifications like NotifyPropertyChanged(); – Joe Sep 03 '16 at 10:02
  • @Joe: The were no error at all ... Or VS didn't showed them ... What do you mean with: _Did you update your binding to point to LatitudeVar instead of Latitude?_ ... NotifyPropertyChanged should be something like OnPropertyChanged(), i thought that the Name isn't important. – Christian Semelis Sep 03 '16 at 10:06
  • @ChristianSemelis why are you re-writing the wheel when the Mvvm namespace has ViewModelBase with INPC already taken care of for you. – mvermef Sep 05 '16 at 01:38
  • Template10.Mvvm, I would reference all samples they are pretty informative and as far as your Session object I would suggesting something along the lines of a SessionService, setup something like the SettingsService, but tailored to your needs that doesn't necessarily have to be exact. – mvermef Sep 05 '16 at 01:45
  • I also suspect you have Binding Errors... – mvermef Sep 05 '16 at 01:51

3 Answers3

4

Woah, that's quite a contrived singleton pattern implementation.

Firstly, the MainPageViewModel instance created in Session.InitializeClient is not the instance that is being used by the view. The XAML

<Page.DataContext>
    <vm:MainPageViewModel x:Name="ViewModel" />
</Page.DataContext>

Will instantiate a new instance of MainPageViewModel so the code

public void UpdateGpsData(Geopoint point, Geopoint geopointOld)
{
    _mpvm.LatitudeVar = point.Position.Latitude.ToString();
}

Will have no effect on the view.

While I would strongly encourage you to remove the attempt at a singleton for the Session and consider using a messaging pattern to communicate between system components, you should be able to get your code working by exposing the Session instance singleton as a readonly property on the MainPageViewModel and binding to that directly as follows:

<TextBlock Text="{Binding Path=Session.Latitude, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>

Of course, this then means your UI is writing directly to a singleton component in your system in which it's probably not desirable to implement INotifyPropertyChanged.

As suggested, I'd strongly recommend rearchitecting and removing the singleton and/or embracing messaging between system components.

ibebbs
  • 1,963
  • 2
  • 13
  • 20
  • When i directly bin to Session, the TextBox keeps blank ... Can you give me hint, how to reachitect, or how the messaging system could work? – Christian Semelis Sep 03 '16 at 11:57
  • The messaging system you could look at would be EventAggregator (Caliburn.Micro/Prism) or Messsenger (MVVM Lite), they are both message passing systems. Problem you might see with message passing is to miss the message though. – mvermef Sep 05 '16 at 01:50
2

Christian, I have knocked up an example of using Caliburn Micro's event aggregator and IoC container to remove dependencies while providing communication between system components. You can find it here.

Key points to note:

  1. The LocationService takes an IEventAggregator as a constructor parameter and has a single public method called Initialize. When Initialize is called, a timer is started to get the current position (stubbed out) every three seconds and publish a LocationChanged message containing the position to the IEventAggregator.

  2. The ShellViewModel also takes the IEventAggregator as a constructor parameter but also implements the interface IHandle<LocationChanged>. When the OnActivate method is called (this is called by Caliburn Micro after the view model is bound to the view) the ShellViewModel calls the Subscribe method of the IEventAggregator passing this as the parameter. The IEventAggregator looks for which IHandle<> interfaces the parameter implements and ensures that, when a message of the generic type (in our case LocationChanged) is published, the Handle method of the implementing type (in our case the ShellViewModel) is called. Inside the Handle method of our ShellViewModel we use the Dispatcher the invoke a method call on the UI thread and then update the Location property to reflect the new location we have just received.

  3. The ShellView binds two text boxes to the Location property of the view model to represent Latitude (Location.Y) and Longitude (Location.X) respectively.

  4. In the AppBootstrapper the IEventAggregator and ILocationService types are registered as 'Singleton' types with the IoC container (here I am using Caliburn's default SimpleContainer). This means only one instance of this type will ever be created and alleviates the need to implement the singleton (anti-)pattern yourself. Finally, in the OnStartup method, the ILocationService is retrieved from the container and the initialize method called to ensure it starts publishing LocationChange messages before the DisplayRootViewFor<IShell> method is called (which causes Caliburn to instantiate the ShellViewModel view model and then locate, instatiate and bind the ShellView view.

If you run the app you will see random locations displayed on the screen every three seconds. This is achieved without the ShellViewModel needing to directly reference the ILocationService and without any (manual) singletons.

Hope it helps.

ibebbs
  • 1,963
  • 2
  • 13
  • 20
0

Isn't this a simpler singleton?

public class MyViewModel
{
    static MyViewModel()
    {
        Instance = new MyViewModel();
    }

    public static MyViewModel Instance { get; }

    private MyViewModel()
    {
        // TODO
    }

    public override async Task OnNavigatedToAsync(object parameter, NavigationMode mode, IDictionary<string, object> suspensionState)
    {
        await Task.CompletedTask;
    }
}
Jerry Nixon
  • 31,313
  • 14
  • 117
  • 233