So I've done this in code:
public partial class Timer : ContentView , INotifyPropertyChanged
{
private int seconds = 30;
private System.Timers.Timer timer;
public Timer()
{
InitializeComponent();
timer = new System.Timers.Timer();
timer.Interval = 1000;
timer.AutoReset = true;
timer.Elapsed += new System.Timers.ElapsedEventHandler(OneSecondPassed);
timer.Enabled = true;
}
private void OneSecondPassed(object source, System.Timers.ElapsedEventArgs e)
{
seconds--;
Time = seconds.ToString();
}
public event PropertyChangedEventHandler PropertyChanged;
public string Time
{
get => seconds.ToString();
set
{
Time = value;
if (PropertyChanged != null)
{
PropertyChanged(this , new PropertyChangedEventArgs("Time"));
}
}
}
}
and then bound my label's text to it in XAML:
<Label BindingContext ="{x:Reference this}"
Text="{Binding Time}"/>
When I start the app, it crashes...I don't really understand how PropertyChanged works, just that INotifyPropertyChanged implements it.Also, when I declare PropertyChanged, it tells me that BindableObject.PropertyChanged already exists, use new in order to hide it.If you could explain how the interface and its event works, I'd be really thankful.
your setter is creating an infinite loop.
set
{
// this will call the setter again, infinitely
Time = value;
...
}
you already have a private variable for seconds, you should use it here
public int Time
{
get => seconds;
set
{
seconds = value;
if (PropertyChanged != null)
{
PropertyChanged(this , new PropertyChangedEventArgs("Time"));
}
}
}
private void OneSecondPassed(object source, System.Timers.ElapsedEventArgs e)
{
Time--;
}
when I declare PropertyChanged, it tells me that BindableObject.PropertyChanged already exists
I see that Timer inherits from ContentView.
ContentView is a BindableObject, so it already implements everything used in binding, such as PropertyChanged. Delete your declaration of PropertyChanged.
OPTIONAL: You could also remove , INotifyPropertyChanged - ContentView does that for you. However it is harmless to leave it there.
Related
I am making a fitness app so I need a timer.I have done this in code-behind:
public partial class Timer : ContentView
{
private int seconds = 30;
private System.Timers.Timer timer;
public Timer()
{
InitializeComponent();
timer = new System.Timers.Timer();
timer.Interval = 1000;
timer.AutoReset = true;
timer.Elapsed += new System.Timers.ElapsedEventHandler(OneSecondPassed);
timer.Enabled = true;
}
private void OneSecondPassed(object source, System.Timers.ElapsedEventArgs e)
{
seconds--;
}
public string Time
{
get => seconds.ToString();
}
}
And then for the UI, I made a label and bound its text property to my Time property:
<Label BindingContext ="{x:Reference this}"
Text="{Binding Time}"/>
//"this" is a reference to my class
When I start the app, the timer remains 30. I know that "seconds" is for sure decreasing, so there must be a problem with the binding.I know I could've just updated the text property of the label inside OneSecondPassed , but I'd like to learn more about data binding.Help?
As Jason said, implementing the INotifyPropertyChanged interface is a good choice.
For your better understanding, I wrote a runnable project that meets your requirements for your reference.
Here is the xaml code:
<StackLayout>
<Label Text="{Binding DateTime}"
FontSize="Large"
HorizontalOptions="Center"
VerticalOptions="Center"/>
</StackLayout>
Here is the cs code:
public partial class MainPage : ContentPage, INotifyPropertyChanged
{
int dateTime;
public event PropertyChangedEventHandler PropertyChanged;
public MainPage()
{
InitializeComponent();
this.DateTime = 30;
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
if (DateTime > 0)
{
DateTime--;
}
return true;
});
BindingContext = this;
}
public int DateTime
{
set
{
if (dateTime != value)
{
dateTime = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("DateTime"));
}
}
}
get
{
return dateTime;
}
}
}
I am trying to bind without success a property called PointerValue to a NeedlePointer.Value progmatically but seem to have got lost somewhere.
The xamarin app basically has a gauge and a start button when the start button is pressed I start the timer. Upon timer elapsed the needle value should increase by on. Easy in XAML but cant figure out how to convert this to code <gauge:NeedlePointer Value="{Binding PointerValue}"
public class StopWatchPage : BaseContentPage
{
private Timer timer;
private double PointerValue
{
get => (double)GetValue(PointerValueProperty);
set => SetValue(PointerValueProperty, value);
}
private static readonly BindableProperty PointerValueProperty =
BindableProperty.Create("PointerValue",
typeof(double), typeof(StopWatchPage), 0d);
public StopWatchPage()
{
this.BindingContext = this;
var needlePointer = new NeedlePointer
{
Value = PointerValue
};
needlePointer.SetBinding(
PointerValueProperty, nameof(PointerValue));
var scale = new Scale{...};
scale.Pointers.Add(needlePointer);
scales.Add(scale);
circularGauge.Scales = scales;
... add gauge to Content etc...
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
this.PointerValue += 1;
}
}
this should work, although the actual name of ValueProperty might be different depending on how NeedlePointer is implemented. The first argument is the name of the control property that you are binding to (the target), and the second is the name of the value property acts as the source.
needlePointer.SetBinding(NeedlePointer.ValueProperty, "PointerValue");
however, if you want the UI to update dynamically, you will also need to have your BindingContext implement INotifyPropertyChanged
There is no need to create a BindableProperty
Solution thanks to #Jason pointing me to the fact that I needed a model that implements INotifyPropertyChanged so code changed to
public class StopWatchViewModel : INotifyPropertyChanged
{
public double PointerValue { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class StopWatchPage : BaseContentPage
{
private Timer timer;
private readonly StopWatchViewModel model = new StopWatchViewModel();
public StopWatchPage()
{
BindingContext = model;
...
needlePointer.SetBinding(NeedlePointer.ValueProperty,
nameof(model.PointerValue));
...
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
model.PointerValue += 1;
}
}
There is a property in my ViewModel whose value is changed in the DoWork method of the BackgroundWorker. When I start the application and click on the button that starts the BackgroundWorker, I see how the value of this property changes. However, when I open a new window, this property retains its default value and is not updated even though the BackgroundWorker is still running.
Her is the code in my ViewModel:
private string currentData;
...
public ViewModel()
{
...
// Property initialised with a default value
currentData = "BackgroundWorker is not running";
...
}
public string CurrentData
{
get { return this.currentData; }
private set
{
if (this.currentData != value)
{
this.currentData = value;
this.RaisePropertyChanged("CurrentData");
}
}
}
private void DoWork(object sender, DoWorkEventArgs e)
{
isUpdating = true;
...
this.CurrentData = "BackgroundWorker is running...";
for (...)
{
...
if(...)
{
this.CurrentData = "value1";
}
else
{
this.CurrentData = "value2";
...
}
}
}
RaisePropertyChanged Method:
private void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML-code for both windows (MainWindow and newtWindow):
<TextBlock Margin="10" MinWidth="250" VerticalAlignment="Center" Text="{Binding CurrentData}" FontSize="12" Foreground="White" HorizontalAlignment="Left" />
BackgroundWorker:
private readonly BackgroundWorker worker;
...
public ImageViewModel()
{
currentData = "BackgroundWorker is not running";
this.worker = new BackgroundWorker();
this.worker.DoWork += this.DoWork;
this.worker.ProgressChanged += this.ProgressChanged;
this.worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_Completeted);
this.worker.WorkerReportsProgress = true;
}
Can you tell what I'm doing wrong and how I can fix it?
You would have to create a private string reference out of your property,
where the property can set the value and it will be saved on the stack,
something like so(this is how wpf get info from text boxes in the text property)
private string _text; //string that is used as a reference which you can plug your new new window
public string Text
{
get
{
return this._text;
}
set
{
this._text = value;
if (null != PropertyChanged)
{
this.PropertyChanged(this, new PropertyChangedEventArgs ("Text"));
}
}
}
I would avoid updating a property, which is bound to the UI, from a background thread. I'm not sure if this will solve your issue, but I would try to use the BackgroundWorker's ReportProgress method to notify your ViewModel about changes of CurrentData. Then in the OnProgressChanged event handler you can set the CurrentData to a new String.
public void ReportProgress(int percentProgress, object userState)
You can put your String into the "userState" object.
Edit
something like this:
public ViewModel()
{
...
backgroundWorker.ReportsProgress = true;
backgroundWorker.ProgressChanged += OnProgressChanged;
...
}
private void DoWork(object sender, DoWorkEventArgs e)
{
isUpdating = true;
...
ReportProgress(0,"BackgroundWorker is running...");
for (...)
{
...
if(...)
{
ReportProgress(0,"value1");
}
else
{
ReportProgress(0,"value2");
...
}
}
}
and
private void OnProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.CurrentData = (string)e.UserState;
}
Ok so from what you've said so far my understanding is as follows:
From your original question:
However, when I open a new window, this property retains its default value and is not updated even though the BackgroundWorker is still running.
From your comment to my previous answer about setting the window's DataContext:
<Window.DataContext> <local:ViewModel /> </Window.DataContext>
When you create a new window, you also create a new instance of your ViewModel. This new instance also has its own BackgroundWorker. When you say "...even though the BackgroundWorker is still running", then this is only true for your first window, since the Backgroundworker from your new window has to be started first.
If you want the same DataContext (and thus the same BackgroundWorker) for both windows, you need to set the DataContext of your new window to the already existing instance of your ViewModel.
I Have an issue with my DependencyProperty. Say you have a timer that updates some UI element, if the callback is called once every 100ms which in turn updates the UI then i have no problem, however, if the timer is set to ~10ms for example, some of the calls will get ignored. I made a small solution that reproduces the problem:
This is a Custom UIElement with a dependency property:
public class CustomLabel : Label
{
public float Range
{
get { return (float)GetValue(MaxRangeProperty); }
set { SetValue(MaxRangeProperty, value); }
}
public static readonly DependencyProperty MaxRangeProperty =
DependencyProperty.Register("Range", typeof(float), typeof(CustomLabel),
new PropertyMetadata(0f, RangePropertyChanged));
private static void RangePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var self = d as CustomLabel;
Debug.WriteLine("CustomLabel");
self.Content = self.Range;
}
}
This is a ViewModel that fires a timer and updates a property which in turn should call the CallBack on the DependencyProperty on CustomLabel.
public class ViewModel : INotifyPropertyChanged
{
Timer timer;
Thread t;
public ViewModel()
{
t = new Thread(() => timer = new Timer(new TimerCallback(CallBack), null, 0, 10));
t.Start();
Range = 100;
}
void CallBack(object state)
{
Range = (new Random()).Next(0, 1000);
}
private float _range;
public float Range
{
get { return _range; }
set
{
if (_range != value)
{
_range = value;
NotifyPropertyChanged();
Debug.WriteLine("ViewModel");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And this is my View where the CustomLabel lives and the ViewModel:
<Window x:Class="TimerTest.MainWindow"
xmlns:local="clr-namespace:TimerTest"
Title="MainWindow">
<Grid>
<local:CustomLabel x:Name="customLabel" Range="{Binding Range}"/>
</Grid>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ViewModel = new ViewModel();
customLabel.DataContext = ViewModel;
}
public ViewModel ViewModel { get; set; }
}
So, I made some Debug.WriteLine() statements on each side of the DependencyProperty, the output looks like this:
100ms 10ms
CustomLabel ViewModel
ViewModel CustomLabel
CustomLabel ViewModel
ViewModel ViewModel
CustomLabel CustomLabel
ViewModel ViewModel
CustomLabel ViewModel
ViewModel ViewModel
CustomLabel ViewModel
ViewModel CustomLabel
Why is this happening and what can I do about it?
Thanks for your time.
The NotifyPropertyChanged event is handled by the Dispatcher, which uses a queue. The dispatcher is processing the events at a slower rate than they are being added to the queue.
Using a DispatcherTimer might allow you to update faster:
DispatcherTimer timer =
new DispatcherTimer(TimeSpan.FromMilliseconds(10),
DispatcherPriority.Normal,
delegate
{
MyCustomLabel.SetValue(MaxRangeProperty, viewModel.Range);
},
Dispatcher);
Also...
The System.Threading.Timer class that you are using does not, by default, have an accuracy capable of 10ms. It will use the operating system timer.
Quoting a Microsoft document on timer resolution:
The default timer resolution on Windows 7 is 15.6 milliseconds (ms)
It is possible to increase the timer resolution using calls to the Windows API, but this can cause battery drain.
I would need accommodate the scenario where I have something like a timer and want to have changed property values reflected in the UI at one moment in time (basically I need Update the UI every x seconds).
I need to know how to add a method to the ViewModel and firing the PropertyChanged event from there.
namespace MyClient.Common
{
public abstract class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T storage, T value, /*[CallerMemberName]*/ String propertyName = null)
{
if (object.Equals(storage, value)) return false;
storage = value;
this.OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged(/*[CallerMemberName]*/ string propertyName = null)
{
var eventHandler = this.PropertyChanged;
if (eventHandler != null)
{
eventHandler(this, new PropertyChangedEventArgs(propertyName));
}
}
public void CallOnPropertyChanged()
{
// what to add here?
}
}
}
App.xaml.cs
namespace MyClientWPF
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
private void DispatcherTimer_Tick(object sender, EventArgs e)
{
App._myDataSource.Load();
App._myDataSource.CallOnPropertyChanged();
// I need to rise OnPropertyChanged here
}
protected override void OnStartup(StartupEventArgs e)
{
// timer on the same thread
System.Windows.Threading.DispatcherTimer dispatcherTimer = new System.Windows.Threading.DispatcherTimer();
dispatcherTimer.Tick += new EventHandler(DispatcherTimer_Tick);
dispatcherTimer.Interval = new TimeSpan(0, 0, 20); // 10 seconds
dispatcherTimer.Start();
base.OnStartup(e);
}
}
}
Use something like this in your code behind?
((TestViewModel)this.DataContext).OnPropertyChanged("PropName");
You can call the OnPropertyChanged(); directly.
But it does seem that there must be a better way of doing what you want to accomplish. I would rather try the advice of #emedbo.