ptd
ptd

Reputation: 21

Xamarin Forms can't change variable value

I want to make a simple Stopwatch app with xamarin forms.

Here is the interface :

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:testvb"
             x:Class="testvb.MainPage">
    <StackLayout>
        <Label Text="{Binding LabelText}">
            <Label.BindingContext>
                <local:GetDuration/>
            </Label.BindingContext>
            <Label.GestureRecognizers>
                <TapGestureRecognizer
                    Command="{Binding TapCommand}">

                </TapGestureRecognizer>
            </Label.GestureRecognizers>
        </Label>
        <Label Text="{Binding SWDuration}">
            <Label.BindingContext>
                <local:GetDuration/>
            </Label.BindingContext>
        </Label>
        <Label Text="{Binding StartTimeText}">
            <Label.BindingContext>
                <local:GetDuration/>
            </Label.BindingContext>
        </Label>

    </StackLayout>

</ContentPage>

And the c#-class:

namespace testvb
{
    class GetDuration : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        static string labeltext = "START";
        static string starttimetext = "Timer hasn't been started";
        ICommand tapCommand;
        static Stopwatch hstopwatch = new Stopwatch();
        string swduration;
        public GetDuration()
        {
            tapCommand = new Command(OnTapped);

            this.SWDuration = hstopwatch.Elapsed.ToString();

            Device.StartTimer(TimeSpan.FromSeconds(1), () =>
            {
                this.SWDuration = hstopwatch.Elapsed.ToString();
               return true;
            });

        }

        public ICommand TapCommand
        {
            get
            {
                return tapCommand;
            }
        }

        void OnTapped (object s)
        {
            if(LabelText == "START")
            {
                LabelText = "STOP";
                StartTimeText = DateTime.Now.ToString();
                hstopwatch.Restart();
            }
            else
            {
                LabelText = "START";
                StartTimeText = "Timer has been stopped";
                hstopwatch.Stop();
                hstopwatch.Reset();
            }
        }

        public string LabelText
        {
            get
            {
                return labeltext;
            }
            set
            {
                if(labeltext != value)
                {
                    labeltext = value;
                    if(PropertyChanged!=null)
                    {
                        PropertyChanged(this, new PropertyChangedEventArgs("LabelText"));                        
                    }
                }

            }
        }

        public string SWDuration
        {
            get
            {
                return swduration;
            }
            set
            {
                if(swduration!=value)
                {
                    swduration = value;
                    if(PropertyChanged!=null)
                    {
                        PropertyChanged(this, new PropertyChangedEventArgs("SWDuration"));
                    }
                }
            }
        }

        public string StartTimeText
        {
            get
            {
                return starttimetext;
            }
            set
            {
                if (starttimetext != value)
                {
                    starttimetext = value;
                    if (PropertyChanged != null)
                    {
                        PropertyChanged(this, new PropertyChangedEventArgs("StartTimeText"));
                    }
                }
            }
        }
    }
}

As from interface I have a label (toogle between start and stop when it is clicked), a label to show the duration, and a label to show the time when the stopwatch was started.

The output on xamarin android simulator:_

I don't understand why it can't work for the last label since the principal ist the same as the first label (start/stop). I would thank you if someone can point out where I misunderstand it or on which part I did wrong on my code.

Thank you.

Upvotes: 0

Views: 1548

Answers (1)

Paul Kertscher
Paul Kertscher

Reputation: 9742

By setting the BindingContext with

<Label.BindingContext>
    <local:GetDuration/>
</Label.BindingContext>

separately on each Label you are creating a new instance of GetDuration for each label. Hence the instance you are invoking TapCommand on will be different to the one that is attached to your labels.

By changeing your XAML to

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:stopwatch"
             x:Class="stopwatch.MainPage">
    <ContentPage.BindingContext>
        <local:GetDuration></local:GetDuration>
    </ContentPage.BindingContext>

    <StackLayout>
        <Label Text="{Binding LabelText}">
            <Label.GestureRecognizers>
                <TapGestureRecognizer
                    Command="{Binding TapCommand}">
                </TapGestureRecognizer>
            </Label.GestureRecognizers>
        </Label>
        <Label Text="{Binding SWDuration}" />
        <Label Text="{Binding StartTimeText}" />
    </StackLayout>
</ContentPage>

you are creating a single GetDuration as the binding context of your page. Unless explicitly set otherwise, the BindingContext will be passed to the children of your page, hence there is no need, to set it for each label (disregarding the fact that it did not work anyway).

Further remarks

Event Invocator

You are raising the event manually from each of your properties. Creating an event invocator that does this for you will save you some lines and make you adhere to DRY more

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

Because of the CallerMemberName attribute, the name of the calling property will be passed automagically (it's syntactic sugar after all and the compiler will do the magic for you). The properties will become

public string StartTimeText
{
    get
    {
        return starttimetext;
    }
    set
    {
        if (starttimetext != value)
        {
            starttimetext = value;
            this.OnPropertyChanged();
        }
    }
}

LabelText for the decision

void OnTapped(object s)
{
    if (LabelText == "START")
    {
        LabelText = "STOP";
        StartTimeText = DateTime.Now.ToString();
        hstopwatch.Restart();
    }
    else
    {
        LabelText = "START";
        StartTimeText = "Timer has been stopped";
        hstopwatch.Stop();
        hstopwatch.Reset();
    }
}

It may be a matter of taste, but I would not let the decision whether to start or to stop the time depend on the value of LabelText, but rather on an internal bool or enum. The way you are doing it, you internal and external behavior. It's OOP after all and you should make use of encapsulation.

Upvotes: 2

Related Questions