I have a TextBlock (caloriesAvailableTextBlock) which I am trying to update. The Button (eatCaloriesButton) is supposed to reduce the number which the TextBlock is bound to by 100. However, the TextBlock will not update. It just remains at 2000. Any ideas what I am missing?
My xaml in HubPage.xaml:
<StackPanel>
<TextBlock TextWrapping="Wrap" Text="Calories Available:" FontSize="24"/>
<TextBlock x:Name="caloriesAvailableTextBlock" Loaded="caloriesAvailableTextBlock_Loaded" TextWrapping="Wrap" Text="{Binding}" FontSize="36"/>
<Button x:Name="eatCaloriesButton" Content="Eat 100 Calories" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" FontSize="18" Click="eatCaloriesButton_Click" FontFamily="Global User Interface"/>
</StackPanel>
My code behind in HubPage.xaml.cs:
public CalorieTracker CalorieTracker { get; set; }
private void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
CalorieTracker = new CalorieTracker();
CalorieTracker.CaloriesAvailable = 2000;
}
private void eatCaloriesButton_Click(object sender, RoutedEventArgs e)
{
CalorieTracker.CaloriesAvailable -= 100;
}
private void caloriesAvailableTextBlock_Loaded(object sender, RoutedEventArgs e)
{
((TextBlock)sender).DataContext = CalorieTracker.CaloriesAvailable;
}
My CalorieTracker.cs class which holds the number which I am updating:
public class CalorieTracker : INotifyPropertyChanged
{
private int caloriesAvailable;
public int CaloriesAvailable
{
get { return caloriesAvailable; }
set { caloriesAvailable = value;
NotifyPropertyChanged("CaloriesAvailable");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
My understanding is that whenever CalorieTracker.CaloriesAvailable is changed, it will let all occurences of that variable know, but that is not what is happening. Any idea why not? Or am I just way off base?
The problem here appears to be how you set up your binding.
You set the whole DataContext to that int for your textblock. This is not what you want to do. For that to update on variable change, well, a lot of stuff would have to be different (for starters the runtime would have to listen on DataContextChanged instead of PropertyChanged).
Instead, set the DataContext for the page to your view model, then bind to a property:
<TextBlock x:Name="caloriesAvailableTextBlock" TextWrapping="Wrap" Text="{Binding CaloriesAvailable}" FontSize="36"/>
private void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
DataContext = CalorieTracker = new CalorieTracker();
CalorieTracker.CaloriesAvailable = 2000;
}
Now your NotifyPropertyChanged will actually do what you expected, and your UI will update. This is a much better fit for the MVVM pattern anyways.
Related
I have been tasked with creating my first UWP App in C#.
The basic idea is to read in an XML file and create objects based on the data read in, then display the properties stored in the object to users in the IU.
Lets say a Person object that has a name, age, and height. I want to display the Person fields after I have read in the data but I can't get anything to show up in the UI after creating the Person object.
I have created a Person class that holds the name, age, height. I have another class that extends ObservableCollection<> and a ItemTemplate that looks for the observable class but currently nothing is showing up on the UI.
Has anyone been through a similar process or know of the correct documentation to read?
Thanks.
First of all in UWP you can choose between two types of binding:
{x:Bind }, is slightly faster at compile time, binds to your Framework Element code-behind class, but it is not as flexible as the other type of binding.
The default mode for this type of binding is OneTime, therefore you will only have your data actually propagated onto your UI, when you construct your object.
{Binding }, in this type of binding where you can only reference variables which exists inside the DataContext of a parent element. The default mode is OneWay.
With that in mind, first of all dealing with a ViewModel which is just a bunch of properties, is different from actually dealing with a Collection, since I don't think the Collection can actually detect alterations on the items itself, but rather on its structure.
Therefore during the Add/Remove process of items in your Collection, you have to actually subscribe/unsubscribe those items to the PropertyChanged EventHandler.
Nevertheless with the following code, i think you should be able to start visualizing updates onto your UI:
VIEWMODEL
public class PersonsObservable<T> : ObservableCollection<Person> where T : INotifyPropertyChanged
{
private PersonsObservable<Person> _personslist;
public PersonsObservable<Person> personslist
{
get { return _personslist; }
set
{
_personslist = value;
_personslist.CollectionChanged += OnObservableCollectionChanged;
}
}
public void OnObservableCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if(e.NewItems != null)
{
foreach (object item in e.NewItems)
((INotifyPropertyChanged)item).PropertyChanged += OnItemPropertyChanged;
}
if(e.OldItems != null)
{
foreach (object item in e.OldItems)
((INotifyPropertyChanged)item).PropertyChanged -= OnItemPropertyChanged;
}
}
public void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, IndexOf((Person)sender));
OnCollectionChanged(args);
}
}
public class Person : INotifyPropertyChanged
{
public Person()
{
_name = "Walter White";
_age = 40;
_height = 180;
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private string _name;
public string name
{
get
{
return _name;
}
set
{
_name = value;
this.OnPropertyChanged();
}
}
private int _age;
public int age
{
get
{
return _age;
}
set
{
_age = value;
this.OnPropertyChanged();
}
}
private int _height;
public int height
{
get
{
return _height;
}
set
{
_height = value;
this.OnPropertyChanged();
}
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
// Add Items
PersonsList.Add(new Person());
}
}
XAML
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel Orientation="Vertical">
<TextBlock Text="DataBinding" Foreground="DarkBlue" FontSize="18" FontWeight="Bold"/>
<ItemsControl ItemsSource="{Binding Mode=TwoWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Name: "/>
<TextBlock Text="{Binding name, Mode=TwoWay}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Age: "/>
<TextBlock Text="{Binding age, Mode=TwoWay}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Height: "/>
<TextBlock Text="{Binding height, Mode=TwoWay}"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="Add Items" Click="Button_Click" Background="Blue" VerticalAlignment="Bottom"/>
</StackPanel>
</Grid>
*Test adding items *
private void Button_Click(object sender, RoutedEventArgs e)
{
// Add Items
PersonsList.Add(new Person());
}
Expose your property and set it to the DataContext of your page (with x:Bind you wouldn't need to do this, but instead you would have to perform a cast for your code to actually compile).
public MainPage()
{
InitializeComponent();
PersonsList = new PersonsObservable<Person>();
this.DataContext = PersonsList;
PersonsList.Add(new Person());
PersonsList.Add(new Person());
}
PersonsObservable<Person> PersonsList { get; set; }
I haven't tested for the situation where one of the items is altered, but you can easily do that, by adding another button (and click event) and actually test if changing one of the items's properties update in your UI.
Anything else, feel free to ask, will be glad to help!
Have a navigation command which needs to display a tooltip on click, while disabled, so that the user knows why it's disabled. The problem I'm having is I have no idea how to pass the TouchDown event from my xaml file to my viewmodel. Is there some way to bind this rather than creating an event in the command.xaml.cs?
Command is structured as follows. I have a single CommandButton.xaml and CommandButton.xaml.cs while everything to set up the button is handled by the VM (text, image, command executed etc) code as an example below.
<Button Focusable="True" Name="Btn1" Command="{Binding CommandToExecute}" Tag="{Binding Text}" Foreground="{x:Null}" Style="{DynamicResource ButtonStyle}" ToolTipService.ShowOnDisabled="true" TouchDown="Btn1_OnTouchDown" >
<Button.ToolTip>
<StackPanel>
<TextBlock>Test</TextBlock>
<TextBlock>Load stencil, or not your choice.</TextBlock>
</StackPanel>
</Button.ToolTip>
<shellModule:AutoGreyableImage Source="{Binding Image}" />
</Button>
As for the code behind, I have that split with the majority of the handler stuff in a base command class as follows.
public abstract class BaseCommand : BindableBase
{
protected IModuleManager ModuleManager { get; set; }
protected IRegionManager RegionManager { get; set; }
protected BaseCommand(IRegionManager regionManager, IModuleManager moduleManager, string pageName = null)
{
RegionManager = regionManager;
ModuleManager = moduleManager;
Text = GetButtonText(pageName + "_BtnTxt");
Image = (ImageSource)Application.Current.FindResource(pageName + "_BtnImg");
}
private string _text;
private ImageSource _image;
public ICommand CommandToExecute => new DelegateCommand<object>(Command, Evaluate);
protected abstract void Command(object obj);
protected virtual bool Evaluate(object obj)
{
return false;
}
public string Text
{
get { return _text; }
set { SetProperty(ref _text, value); }
}
public ImageSource Image
{
get { return _image; }
set { SetProperty(ref _image, value); }
}
protected string GetButtonText(string key)
{
string uiString;
var locExtension = new LocTextExtension
{
Key = "Resources",
ResourceIdentifierKey = key
};
locExtension.ResolveLocalizedValue(out uiString);
return uiString;
}
}
and then the command specific stuff in the viewmodel.
public class Page1CommandViewModel : BaseCommand, IPage1CommandViewModel
{
public Page1CommandViewModel(IRegionManager regionManager, IModuleManager moduleManager) : base( regionManager, moduleManager, PageNames.Page1 )
{
}
protected override void Command(object obj)
{
Task.Run(() =>
{
ModuleManager.LoadModule(ModuleNames.Page1Module);
RegionManager.RequestNavigate(RegionNames.ContentRegion, new Uri(PageNames.Page1, UriKind.Relative));
});
}
}
If anyone could point me in the right direction it'd be greatly appreciated.
Maybe instead of disabling the button, re-point the button to a different method, which would then display your error/tooltip message. (You could then pass in the string stating the reason for the inactivity in your method paramaters/variables.)
I would also advise you change the class/visual properties of the button so that it looks disabled.
After much googling, I've come up with a solution myself, thanks in part to comments others had made leading me in the right direction. Wrapped up my button in a contentControl, and instead have applied the tooltip to this.
<ContentControl MouseDown="ContentControl_MouseDown">
<ContentControl.ToolTip>
<ToolTip Placement="Mouse" Content="Testing" />
</ContentControl.ToolTip>
<Button Focusable="True" x:Name="Btn1" Command="{Binding CommandToExecute}" Tag="{Binding Text}" Foreground="{x:Null}" Style="{DynamicResource ButtonStyle}" ToolTipService.ShowOnDisabled="true">
<shellModule:AutoGreyableImage Source="{Binding Image}" />
</Button>
</ContentControl>
And on the button.xaml.cs put in events to handle timings of the button click etc.
Timer Timer { get; set; }
ToolTip toolTip { get; set; }
public CommandButton()
{
InitializeComponent();
Timer = new Timer {Interval = 3000};
Timer.Elapsed += OnTimerElapsed;
}
private void CloseToolTip()
{
if (toolTip != null)
{
toolTip.IsOpen = false;
}
}
private void OnTimerElapsed(object sender, ElapsedEventArgs e)
{
Timer.Stop();
Application.Current.Dispatcher.BeginInvoke((Action)CloseToolTip, DispatcherPriority.Send);
}
private void ContentControl_MouseDown(object sender, MouseButtonEventArgs e)
{
toolTip = ((ToolTip)((Control)sender).ToolTip);
toolTip.IsOpen = true;
Timer.Start();
}
timers taken from the following location.
https://social.msdn.microsoft.com/Forums/vstudio/en-US/9e3eb4ab-ed0f-40ad-ad47-a1fff8e0fe8d/tooltips-in-wpf-touch-applications?forum=wpf
This allows the button to be disabled, and the tooltip to still display on click. All I need to do now is wrap up the tooltip contents in a binding and disable the tooltip on hover (not required) and it's all solved.
Leaving question open for the time being however, as a better solution may present itself.
The same questions has been asked many times on this site and I have read most of them. But I have a special problem (maybe?) that couldn't figure it out after hours of struggling and reading SO posts.
The problem is -simply explained, I have a WPF form which contains a Connect button. If this button is pressed a textblock must appear on that form, displaying the word "Connecting...". Upon pressing the button, some handshaking operations are done in the associated C# code which takes some time. If the program fails to connect, the textblock must change to "Failed!". Otherwise, it changes to "Succeed."
Now for this simple problem, I wrote in my XAML:
<Window x:Class="WpfTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="300" Width="200">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Button x:Name="connecting" Content="Connect" FontWeight="Bold" Click="startConnection"
Width="60" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Row="0"/>
<TextBlock x:Name="comm_stat" Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center"
Text="{Binding Content}"/>
</Grid>
</Window>
And the C# code (inspired by this answer):
using System;
using System.Text;
using System.Windows;
using System.ComponentModel;
namespace WpfTest
{
public class DynamicObj : INotifyPropertyChanged
{
public DynamicObj() : this(string.Empty) { }
public DynamicObj(string txt) { Content = txt; }
private string _name;
public string Content
{
get { return _name; }
set {
_name = value;
OnPropertyChanged("Content");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string PropertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
comm_stat.DataContext = new DynamicObj();
}
private void startConnection(object sender, RoutedEventArgs e)
{
comm_stat.Text = "Connecting...";
bool connect2device = false;
// do the handshaking operations. the result is then assigned to connect2device
comm_stat.Text = connect2device ? "Succeed." : "Failed!";
// some other operations
}
}
}
Now the problem is, whenever I click the button, no text is appeared in the textblock. Because the program waits for the startConnection method to reach its end and then updates the bonded textblock. But I want the textblock to change right after pressing the button. How can I do this?
You can use BackgroundWorker as such:
bool connect2device = false;
private void startConnection(object sender, RoutedEventArgs e)
{
comm_stat.Text = "Connecting...";
// do the handshaking operations. the result is then assigned to connect2device
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += DoWork;
worker.RunWorkerCompleted += Completed;
worker.RunWorkerAsync();
}
private void Completed(object sender, RunWorkerCompletedEventArgs e)
{
comm_stat.Text = connect2device ? "Succeed." : "Failed!";
}
private void DoWork(object sender, DoWorkEventArgs e)
{
//Change with actual work.
Thread.Sleep(1000);
connect2device = true;
}
One side note is that you actually do not use bindings to change the text. comm_stat.Text = "Connecting..."; sets the text property directly and the DynamicObj object is not used at all. It might be good for you to read a few tutorial on MVVM.
I'm extremely new to develop WP8 in C#. I'm trying to write a simple app which will fetch data from a web page, and then update a TextBlock continuously, possibly every second.
Currently, I am able to fetch data from the web page, but cannot update the TextBlock, even though I have used INotifyPropertyChanged. The TextBlock is only updated once when the application started.
The cs file of MachineData:
public class MachineData : INotifyPropertyChanged
{
public string CurrentValue
{
get
{
return _currentValue;
}
set
{
_currentValue = value;
NotifyPropertyChanged("CurrentValue");
}
}
private string _currentValue;
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Here is the code that I used for fetching data:
foreach (MachineData md in Items)
{
var wc = new WebClient();
wc.DownloadStringCompleted += (sender, e) =>
{
string data = (string)e.Result;
md.CurrentValue = data; // Update the value of machine
wc.DownloadStringAsync(new Uri(address));
};
wc.DownloadStringAsync(new Uri(address));
}
The xaml snippet:
<DataTemplate>
<StackPanel Margin="0,0,0,17">
<TextBlock Text="{Binding CurrentValue}" TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
</StackPanel>
</DataTemplate>
I'm assuming you are using some list like control, such as ListBox, GridView, etc...
What you need to do is bind your Items to ItemSource to the control then you'll get an update.
I don't currently see how you are binding the MachineData to a control so that might be my first guess is to why it doesn't work for you.
Here is some working code.
XAML:
<Grid Background="White">
<ListBox x:Name="MyListBox">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,17">
<TextBlock Text="{Binding CurrentValue}" TextWrapping="Wrap"/>
<Button Click="Button_Click">click</Button>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
Code behind:
public sealed partial class MainPage : Page
{
List<MachineData> Items = new List<MachineData>();
public MainPage()
{
this.InitializeComponent();
if (System.Diagnostics.Debugger.IsAttached)
{
Application.Current.DebugSettings.EnableFrameRateCounter = false;
}
Items.Add(new MachineData());
MyListBox.ItemsSource = Items;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
foreach (MachineData md in Items)
{
md.CurrentValue = "Update Text";
}
}
}
Here, if you press the button, the MachineData is set with "Update Text" and you should see the text update.
My class "ship" should change value of property "ShipOnePos" continuously. This value is binded to one of UI element (image) property. I like to update my UI according to "ShipOnePos" (when it is changed). So for this reason I use interface INotifyPropertyChanged, but UI is not updating, and PropertyChanged value is always null.
Can You advice what I miss, or what is wrong in this implementation ?
Class:
namespace DzienNaWyscigach
{
public class ship : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private int MyShipOnePos;
public int ShipOnePos
{
get { return MyShipOnePos; }
set { MyShipOnePos = value;
NotifyPropertyChanged();
}
}
public void ShipsMovment()
{
DispatcherTimer timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromMilliseconds(500);
timer.Tick += Timer_Tick;
timer.Start();
}
private void Timer_Tick(object sender, EventArgs e)
{
ShipOnePos= ShipOnePos+10;
}
}
}
UI Binding
<Image x:Name="ShipOne" HorizontalAlignment="Left" Height="71" Margin="31,32,0,0" VerticalAlignment="Top" Width="80" Source="Asets/ship1.png" RenderTransformOrigin="0.5,0.5">
<Image.DataContext>
<local:ship/>
</Image.DataContext>
<Image.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform X="{Binding Path=ShipOnePos, Mode=OneWay}"/>
</TransformGroup>
</Image.RenderTransform>
Code behind
private void BTN_Start_Click(object sender, RoutedEventArgs e)
{
ship Ships = new ship();
Ships.ShipsMovment();
}
The ship you are creating in your click isn't the ship you bound your Image to. You should probably create a ship as a resource and in your Click event retrieve that existing ship and start it moving. Or else, you might want to have a parent VM with a ship property.
So for example, you could do something like this:
<Window.Resources>
<local:ship x:Key="myShip"/>
</Window.Resources>
....
<Image x:Name="ShipOne"
HorizontalAlignment="Left" Height="71"
Margin="31,32,0,0" VerticalAlignment="Top" Width="80" Source="Asets/ship1.png"
RenderTransformOrigin="0.5,0.5"
DataContext="{StaticResource myShip}">
...
</Image>
And then in your click handler:
private void BTN_Start_Click(object sender, RoutedEventArgs e)
{
// Note: it might be worth caching this in a field or property
ship Ships = FindResource("myShip") as ship;
Ships.ShipsMovment();
}
Or alternatively, using the parent VM idea:
public class ParentVM : INotifyPropertyChanged
{
private ship _currentShip;
public ship CurrentShip
{
get { return _currentShip; }
set { _currentShip = value; NotifyPropertyChanged(); }
}
// ....
}
And then you could do:
<Window.Resources>
<local:ParentVM x:Key="myVM"/>
</Window.Resources>
...
<Grid DataContext="{StaticResource myVM}">
....
<Image DataContext="CurrentShip" ...>
....
</Image>
</Grid>
And:
// Note: this might be better handled with a command rather than a click handler
private void BTN_Start_Click(object sender, RoutedEventArgs e)
{
// Note: it might be worth caching this in a field or property
ParentVM vm = FindResource("myVM") as ParentVM;
vm.CurrentShip = new ship();
vm.CurrentShip.ShipsMovment();
}