I am trying to make my very first Silverlight App ever, but I can't get the LogOn function to work, can you help me? This should properly be super simple for all of you, I will show you my two files: LogOn.xaml.cs and LogOnViewModel.cs
Apparently the problem is that UserId gets not set early enough to be availble in LogOn.xaml.cx when I need it, can you help me make it work, that would lift my moment quite a bit :-)
public partial class LogOn : PhoneApplicationPage
{
public LogOn()
{
InitializeComponent();
this.DataContext = LogOnViewModel.Instance;
}
private void btnLogOn_Click(object sender, RoutedEventArgs e)
{
if ((!string.IsNullOrEmpty(txtEmailAddress.Text)) && (!string.IsNullOrEmpty(txtPassword.Password)))
{
txbLogonMessage.Text = "";
LogOnViewModel.Instance.UserLogin(txtEmailAddress.Text, txtPassword.Password);
if (LogOnViewModel.Instance.UserId > 0)
NavigationService.Navigate(new Uri("/_2HandApp;component/Views/Main.xaml", UriKind.Relative));
else
txbLogonMessage.Text = "Login was unsuccessful. The user name or password provided is incorrect. Please correct the errors and try again. ";
}
}
}
public sealed class LogOnViewModel : INotifyPropertyChanged
{
public static LogOnViewModel Instance = new LogOnViewModel();
//public static int userId;
private SHAServiceClient WS;
private int userId;
public int UserId
{
get
{
return userId;
}
set
{
userId = value;
this.RaisePropertyChanged("UserId");
}
}
private LogOnViewModel()
{
WS = new SHAServiceClient();
WS.UserLoginCompleted += new EventHandler<UserLoginCompletedEventArgs>(WS_UserLoginCompleted);
}
void WS_UserLoginCompleted(object sender, UserLoginCompletedEventArgs e)
{
if (e.Error == null)
{
this.UserId = e.Result;
}
}
public void UserLogin(string email, string password)
{
WS.UserLoginAsync(email, password);
}
/* Implementing the INotifyPropertyChanged interface. */
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler propertyChanged = this.PropertyChanged;
if ((propertyChanged != null))
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The cause of the problem is what has been highlighted by #flq. You're making an asynchronous call, meaning that you won't get the expected result right away (in your case, the UserId being assigned), but instead, you can subscibe to the Completed event (or provide a callback) to handle things when the asynchronous task finishes.
Now, the "MVVM way" to do this (or at least what I would do) is as follows: first of all, go get MVVM Light! it's a lightweight MVVM framework which would be very helpful. You should have your ViewModel class implement the ViewModelBase base class from MVVMLight, this would provide the change notification and messaging as well as other useful stuff. Then, you should encapsulate the login functionality in a command to be able to wire up it up from xaml, for that you can use MVVMLight's RelayCommand. Once the login is complete, you can just send a message to your view letting it know that (in a pretty decoupled way), and the view can simply initiate the navigation.
Here's the bits of code for that:
public class LogOnViewModel : ViewModelBase
{
private SHAServiceClient WS;
public LogOnViewModel()
{
WS = new SHAServiceClient();
WS.UserLoginCompleted += new EventHandler<UserLoginCompletedEventArgs>(WS_UserLoginCompleted);
LoginCommand = new RelayCommand(UserLogin);
}
private int userId;
public int UserId
{
get { return userId; }
set
{
userId = value;
RaisePropertyChanged(()=>UserId);
}
}
private int password;
public int Password
{
get { return password; }
set
{
password = value;
RaisePropertyChanged(()=>Password);
}
}
private int username;
public int Username
{
get { return username; }
set
{
username = value;
RaisePropertyChanged(()=>Username);
}
}
private int loginErrorMessage;
public int LoginErrorMessage
{
get { return loginErrorMessage; }
set
{
loginErrorMessage = value;
RaisePropertyChanged(()=>LoginErrorMessage);
}
}
void WS_UserLoginCompleted(object sender, UserLoginCompletedEventArgs e)
{
if (e.Error == null)
{
this.UserId = e.Result;
// send a message to indicate that the login operation has completed
Messenger.Default.Send(new LoginCompleteMessage());
}
}
public RelayCommand LoginCommand {get; private set;}
void UserLogin()
{
WS.UserLoginAsync(email, password);
}
}
for the xaml:
<TextBox Text="{Binding Username, Mode=TwoWay}"/>
<TextBox Text="{Binding Password, Mode=TwoWay}"/>
<Button Command="{Binding LoginCommand}"/>
<TextBlock Text="{Binding LoginErrorMessage}"/>
in the code behind:
public partial class LogOn : PhoneApplicationPage
{
public LogOn()
{
InitializeComponent();
this.DataContext = new LogOnViewModel();
Messenger.Default.Register<LoginCompletedMessage>(
this,
msg=> NavigationService.Navigate(
new Uri("/_2HandApp;component/Views/Main.xaml",
UriKind.Relative) );
}
....
}
You can see that there is a little bit more (but straightforward) code in the ViewModel and less in the code behind. This also took advantage of DataBinding which is in the heart of MVVM.
Hope this helps :)
P.S: the LoginCompletedMessage class is just an empty class in this case (used just to define the type message), but you can use it to send more info (maybe you still want to have the UserId sent)
Well, you're calling an async version of a login WS.UserLoginAsync, which means the execution moves on and indeed there is no user id when you check for it.
You aren't really doing MVVVM here, but anyway, let's go with the flow. Have an event on your "Viewmodel" that is raised when the login process is finished (WS_UserLoginCompleted). You can handle it and trigger Navigation in an event-handler of that event.
Related
I basically used a Model's (UserAccount) Property from my ViewModel(CreateAccountViewModel) to bind to my View, and call to my Command (CreateAccountCommand).
My Model(UserAccount):
public class UserAccount : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int _id;
private string _username;
private string _password;
private DateTime _dateTime;
public int Id
{
get { return _id; }
set { _id = value; OnPropertyChanged(nameof(Id)); }
}
public string Username
{
get { return _username; }
set { _username = value; OnPropertyChanged(nameof(Username)); }
}
public string Password
{
get { return _password; }
set { _password = value; OnPropertyChanged(nameof(Password)); }
}
public DateTime DateCreated
{
get { return _dateTime; }
set { _dateTime = value; OnPropertyChanged(nameof(DateCreated)); }
}
public virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
My ViewModel(CreateAccountViewModel):
public class CreateAccountViewModel: ViewModelBase
{
private UserAccount _userAccount;
public UserAccount CurrentUserAccount
{
get { return _userAccount; }
set { _userAccount = value; OnPropertyChanged(nameof(CurrentUserAccount)); }
}
public ICommand CreateAccountCommand{ get; }
public CreateAccountViewModel()
{
CreateAccountCommand= new CreateAccountCommand(this, Test);
CurrentUserAccount = new UserAccount();
}
public void Test()
{
MessageBox.Show("Random Message");
//I'm going to put my Create functionality here
}
}
My View (CreateAccountView):
<!--The TextBox for username-->
<TextBox Grid.Column="1"
Margin="10,0,0,0"
Text="{Binding Path=CurrentUserAccount.Username, UpdateSourceTrigger=PropertyChanged}" />
<!--The PasswordBox for password-->
<components:BindablePasswordBox Grid.Column="1"
Margin="10,0,0,0"
Password="{Binding Path=CurrentUserAccount.Password, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
Grid.ColumnSpan="2" />
<!--The Create user button-->
<Button Grid.Row="2"
Margin="0,20,0,0"
HorizontalAlignment="Center"
Command="{Binding CreateAccountCommand}"
Content="Create Account" />
My Command(CreateAccountCommand):
public class CreateAccountCommand: ICommand
{
private readonly CreateAccountViewModel _viewModel;
private readonly Action RunCommand;
public CreateAccountCommand(CreateAccountViewModel viewModel , Action runCommand)
{
_viewModel = viewModel;
_viewModel.PropertyChanged += ViewModel_PropertyChanged;
RunCommand = runCommand;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
//This is supposed to check whether the Username textbox and Password passwordbox is blank (if both of them are blank, the button should be disabled, else disabled
return !string.IsNullOrEmpty(_viewModel.CurrentUserAccount.Username) && !string.IsNullOrEmpty(_viewModel.CurrentUserAccount.Password);
}
public void Execute(object parameter)
{
RunCommand();
}
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
CanExecuteChanged?.Invoke(this, new EventArgs());
}
}
My PasswordBox is bindable because I created a custom PasswordBox with DependencyProperty:
public partial class BindablePasswordBox : UserControl
{
public static readonly DependencyProperty PasswordProperty =
DependencyProperty.Register("Password", typeof(string), typeof(BindablePasswordBox),
new PropertyMetadata(string.Empty));
public string Password
{
get { return (string)GetValue(PasswordProperty); }
set { SetValue(PasswordProperty, value); }
}
public BindablePasswordBox()
{
InitializeComponent();
}
//This method will notify us, whenever a password in our passwordBox changes
private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
{
Password = passwordBox.Password; //sets the value of the DependencyProperty (PasswordProperty)
}
}
My problem here, is that, the button in my View does not change enable/disable even if I set my command's CanExecute to do so. Am I missing something obvious here? I really have to ask because I've been stuck here since yesterday. (My Main goal here is to disable the Create Account button if the Textbox and PasswordBox have no input. Any solutions are okay)
Lets do a small refactoring.
use CallerMemberNameAttribute (see here how) to have shorter property setters in vm;
write once reusable ICommand implementation and use it for all commands, see DelegateCommand;
rise command CanExecuteChanged in vm when you change one of command canExecuted condition;
UserAccount needs notifications (you have done it in the edit), if it's a model, then you need an extra vm to act as a wrapper, otherwise you wouldn't be able to catch changes done by the bound controls;
Since the properties of UserAccount are part of command canExecuted, you need to monitor for them.
With all changes your button using the command should be property enabled/disabled.
Below is pseudo-code (can contain mistakes):
public class CreateAccountViewModel: ViewModelBase
{
UserAccount _userAccount;
public UserAccount CurrentUserAccount
{
get => _userAccount;
set
{
// unsubscribe
if(_userAccount != null)
_userAccount -= UserAccount_PropertyChanged;
_userAccount = value;
// subscribe
if(_userAccount != null)
_userAccount += UserAccount_PropertyChanged;
// notifications
OnPropertyChanged(); // shorter syntax with CallerMemberNameAttribute
CreateAccountCommand.RaiseCanExecuteChanged();
}
}
public ICommand CreateAccountCommand { get; }
public CreateAccountViewModel()
{
CurrentUserAccount = new UserAccount();
CreateAccountCommand = new DelegateCommand(Test,
o => !string.IsNullOrEmpty(CurrentUserAccount.Username) && !string.IsNullOrEmpty(CurrentUserAccount.Password));
}
void Test(object parameter)
{
MessageBox.Show("Random Message");
//I'm going to put my Create functionality here
}
void UserAccount_PropertyChanged(object sender, NotifyPropertyChangedEventArgs e) =>
CreateAccountCommand.RaiseCanExecuteChanged(); // rise always of check for specific properties changes
}
The CreateAccountCommand hooks up en event handler to the view model's PropertyChanged but there is no such event raised when you set the Username and Password properties of the UserAccount object.
Either implement INotifyPropertyChanged in UserAccount or bind to wrapper properties of the CreateAccountViewModel:
public string Username
{
get { return _userAccount?.Username; }
set
{
if (_userAccount != null)
_userAccount.Username = value;
OnPropertyChanged();
}
}
If you decide to implement INotifyPropertyChanged in UserAccount, you still need to notify the command when the properties have been updated.
Since your CurrentUserAccount property may be set to a new value dynamically, you should remove and add the event handler dynamically:
private UserAccount _userAccount;
public UserAccount CurrentUserAccount
{
get { return _userAccount; }
set
{
if (_userAccount != null)
_userAccount.PropertyChanged -= OnUserAccountPropertyChanged;
_userAccount = value;
if (_userAccount != null)
_userAccount.PropertyChanged += OnUserAccountPropertyChanged;
OnPropertyChanged(nameof(CurrentUserAccount));
}
}
private void OnUserAccountPropertyChanged(object sender, PropertyChangedEventArgs e) =>
OnPropertyChanged(null);
I'm trying to get my head around data binding in Xamarin.Forms. I've read lots of the guides and played with some examples and I am now trying to implement some of my own basic binding.
I've got a Strings file in which I've declared an empty variable:
public static class Strings
{
public static string UserDisplayName;
}
On load of my View, it runs an async function to grab data from a Azure SQL DB which then populates the string
Strings.UserDisplayName = user.FirstName;
In my view page I've bound a label to a variable userDisplayNm
<Label Text="{Binding UserDisplayNm}"></Label>
In my ViewModel I have the following to set UserDisplayNm, however it only ever returns "Welcome, ". How do i get it to fire this again after the sync service has completed & the Strings.UserDisplayname value changes? I think I'm missing a link to an PropertyChanged event or something?
namespace Travel_Comp.ViewModels
{
public sealed class MenuViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public MenuViewModel()
{
this.UserDisplayNm = Strings.UserDisplayName;
}
public string UserDisplayNm
{
set
{
if (Strings.UserDisplayName != value)
{
value = Strings.UserDisplayName;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("UserDisplayNm"));
}
}
}
get
{
return "Welcome, " + Strings.UserDisplayName;
}
}
}
}
EDIT:
Thanks for your replies. I think I'm getting closer based on the replies below, here is what I've now got, although The MenuViewModel.LoadAsync() is throwing an error "Inaccessible due to its protection level", so i can't compile to check it yet. Is this what you were suggesting & any ideas on the Protection level issue??
Strings file:
public static class Strings
{
public static string UserDisplayName;
}
ViewModel:
namespace Travel_Comp.ViewModels
{
public sealed class MenuViewModel : INotifyPropertyChanged
{
//Azure sync process
ServerManager manager;
public event PropertyChangedEventHandler PropertyChanged;
public MenuViewModel()
{
//Initial set of UserDisplayNm
this.UserDisplayNm = Strings.UserDisplayName;
}
async void LoadAsync()
{
try
{
//Run process to populate Strings.UserDisplayNm, set Syncitems to true to sync with Server
foreach (var user in await manager.GetUsersAsync(syncItems: true))
{
Strings.UserDisplayName = user.FirstName;
}
}
catch (Exception e)
{
Console.WriteLine($"Error while retrieving user name: {e}");
}
}
public string UserDisplayNm
{
set
{
if (Strings.UserDisplayName != value)
{
value = Strings.UserDisplayName;
if (PropertyChanged != null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UserDisplayNm)));
}
}
}
get
{
return "Welcome, " + Strings.UserDisplayName;
}
}
}
}
View:
protected override async void OnAppearing()
{
base.OnAppearing();
ViewModels.MenuViewModel.LoadAsync();
}
So if you're looking some guidance for MVVM, you should know that usually you put your dependencies in your view model constructor, here your Azure service.
Also you could use a existing MVVM framework that will make things easy for you, like Prism or FreshMVVM.
But if you want to go for full vanilla you can also call your vm code from the view code behind.
So I'm suggesting this modification to your MenuViewModel:
private IAzureService _azureService;
private string _userDisplayNm;
public MenuViewModel(IAzureService azureService)
{
_azureService = azureService;
}
public string UserDisplayNm
{
get
{
return _userDisplayNm;
}
set
{
if (_userDisplayNm != value)
{
_userDisplayNm = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UserDisplayNm)));
}
}
}
public async void LoadAsync()
{
try
{
UserDisplayNm = await _azureService.GetUserName();
}
catch (Exception exception)
{
Debug.WriteLine($"Error while retrieving user name: {exception}")
}
}
Then in you view code behind:
void OnAppearing()
{
_menuViewModel.LoadAsync();
}
To resolve the question: Inaccessible due to its protection level, you can try to add the public access modifier before the function of LoadAsync.
public async void LoadAsync(){
//....
}
And I have created a simple demo to simulate your code.
The main code is:
public sealed class TestModel: INotifyPropertyChanged
{
//*******************************************
string _userDisplayName;
public string UserDisplayName {
set { SetProperty(ref _userDisplayName, value); }
get { return _userDisplayName; }
}
public async void LoadAsync()
{
try
{
UserDisplayName = "updated value: Angela";
Strings.UserDisplayName = UserDisplayName;
}
catch (Exception exception)
{
Debug.WriteLine($"Error while retrieving user name: {exception}");
}
}
bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Object.Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
xaml
<Label Text="{Binding UserDisplayName }" BackgroundColor="Yellow"
VerticalOptions="Center" HorizontalOptions="Fill" HeightRequest="50" />
<Button Text="update the Label value" Clicked="Button_Clicked"/>
And use like this:
public partial class MyPage1 : ContentPage
{
TestModel model;
public MyPage1 ()
{
InitializeComponent ();
model = new TestModel();
BindingContext = model;
}
private void Button_Clicked(object sender, EventArgs e)
{
model.LoadAsync();
}
}
The effect is:
I have a class that gathers information about a machine (this is an example - in total GetInfo() may take minutes to run):
public class Scanner
{
public void GetInfo()
{
this.Name = GetName();
this.OS = GetOS();
}
public string Name { get; set; }
public string OS { get; set; }
public string Status { get; set; }
private string GetName() { this.Status = "Getting computer name"; /*More operations*/ }
private string GetOS() { this.Status = "Getting OS"; /*More operations*/ }
}
This is called by a form that needs to provide status feedback to the user.
TextBox1.Text = scanner.Status;
My question is, to achieve this, what is the best way to implement threading so that the application remains responsive?
I have got a BackgroundWorker running in the form and calling GetName(), GetOS() etc itself and that works fine, but the code isn't very repeatable - to keep maintenance low I want to keep a GetInfo() method in the class so if I need to run a scan from elsewhere theres only one piece of code that knows about how to.
I could move the BackgroundWorker in to GetInfo(), but then how would the form be able to check the Status property without doing a loop and locking up the UI?
Maybe have a BackgroundWorker in the form run GetInfo() and then run another Worker that would check Status and update the form if a change is detected?
This is my first proper go at Threading and can't get my head around what, I think, is a fairly common task so any help would be appreciated.
Note: I'm also well open to suggestions for other ways to implement the Status property - hopefully you get what I'm trying to achieve.
/edit: Some clarification.
Scanner.GetInfo() would be called manually, for example on a form button click. GetInfo() would then start populating the objects properties as it goes gathering information, and might take 5 minutes to complete.
I need to be able to keep the user up to date on its status, and the only way I can think of that happening (with my current knowledge) is for GetInfo() to update a 'Scanner.Status' property, which the form can then check at interval/within a loop and update the UI when it changes.
How about using INotifyPropertyChanged with a thread in the class as follows:
public class Scanner : INotifyPropertyChanged
{
private string _Name, _OS;
public event PropertyChangedEventHandler PropertyChanged;
public string Name
{
get
{
return _Name;
}
set
{
if (value != _Name)
{
_Name = value;
NotifyPropertyChanged("Name");
}
}
}
public string OS
{
get
{
return _OS;
}
set
{
if (value != _OS)
{
_OS = value;
NotifyPropertyChanged("OS");
}
}
}
public void GetInfo()
{
_Name = string.Empty;
_OS = string.Empty;
new Thread(() => this.Name = GetName()).Start();
new Thread(() => this.OS = GetOS()).Start();
}
private void NotifyPropertyChanged(string pName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(pName));
}
}
private string GetName()
{
return "long name operation here";
}
private string GetOS()
{
return "long OS operation here";
}
}
Then in your form you could use the following:
Scanner m_Scanner = new Scanner();
public void Main()
{
m_Scanner.PropertyChanged += UpdateGUI;
m_Scanner.GetInfo();
}
private void UpdateGUI(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "OS")
txtOs.Text = m_Scanner.OS;
else if (e.PropertyName == "Name")
txtName.Text = m_Scanner.Name;
}
So I am trying to implement the MVVM pattern in a simple sample app. Essentially my app allows a user to choose from a list of search providers in a SettingsPage, and then in the MainPage when the user clicks the 'search' button he or she will be navigated to the search provider's website. Everything seems to work ok, no errors, except when navigating directly back to MainPage from SettingsPage the search property does not seem to be updated. Everything is fine though when the application is completely exited and launched fresh. What I have is as follows
MainPage.xaml.cs
void search_Click(object sender, EventArgs e)
{
TheBrowser.Navigate(App.ViewModel.SearchProvider.Address);
}
App.xaml.cs
private static MainViewModel viewModel = null;
public static MainViewModel ViewModel
{
get
{
// Delay creation of the view model until necessary
if (viewModel == null)
viewModel = new MainViewModel();
return viewModel;
}
}
MainViewMode.cs
public ListItem SearchProvider { get; private set; }
public MainViewModel()
{
SearchProvider = Settings.SearchProvider.Value;
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
and in my SettingsPage is where I am allowin ga user to select a search provider
SettingsPage.xaml.cs
private void PopulateSearchProviderList()
{
searchProviderList = new ObservableCollection<ListItem>();
searchProviderList.Add(new ListItem { Name = "Bing", Address = "http://www.bing.com" });
searchProviderList.Add(new ListItem { Name = "Google", Address = "http://www.google.com" });
SearchProviderListPicker.ItemsSource = searchProviderList;
}
private void stk_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
if (SearchProviderListPicker.SelectedIndex != -1)
{
var selectedItem = (sender as StackPanel).DataContext as TestApp.Classes.ListItem;
Settings.SearchProvider.Value = selectedItem; //Setting the search provider
}
}
and finally my ListItem class which is fairly straightforward
ListItem.cs
public string Name
{
get;
set;
}
public string Address
{
get;
set;
}
So essentially I am not updating the ViewModel correctly based on the SettingsPage, but I am unsure of how to go about this properly.
You have to call the OnNotifyPropertyChanged("propertyName") for the item to update in the UI.
For example (assuming the Name and Address properties are bound to your UI elements.)
private string name;
private string address;
public string Name
{
get { return name;}
set {
name = value;
OnNotifyPropertyChanged("Name");
}
}
public string Address
{
get { return address; }
set {
address = value ;
OnNotifyPropertyChanged("Address");
}
}
There are a few issues I can see. We'll start from there.
Your MainViewModel needs to implement INotifyPropertyChanged see here
Your SearchProvider setter needs to raise PropertyChanged
You need to set the value of the SearchProvider. Currently that is only performed in the constructor which is probably why you are seeing things working on app startup only.
You need to make sure you are correctly binding the value of SearchProvider in your xaml. If you post your xaml we can check that out too.
In your ViewModel, add:
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string caller = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
}
Update the SearchProvider property to something like:
private ListItem searchProvider;
public ListItem SearchProvider
{
get { return searchProvider; }
set
{
searchProvider = value;
OnPropertyChanged();
}
}
I'm trying to create WP7 application based on MVVM pattern but I have problem with refreshing bind content of TextBlock. In the current state I need to reopen page to refresh content. I think that it's related to setting data context but I couldn't fix it.
PropertyChangedEventHandler in ViewModel.cs
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
if (null != PropertyChanged)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private string _txtStatus = "";
public string TxtStatus
{
get { return _txtStatus; }
set
{
_txtStatus = value;
NotifyPropertyChanged("TxtStatus");
}
}
ViewModel Property in App.xaml.cs
public partial class App : Application
{
private static ViewModel _viewModel { get; set; }
public static ViewModel ViewModel
{
get { return _viewModel ?? (_viewModel = new ViewModel()); }
}
Setting DataContext in StatusPage.xaml.cs
public partial class Status : PhoneApplicationPage
{
public Status()
{
InitializeComponent();
DataContext = App.ViewModel;
}
Binding in StatusPage.xaml
<TextBlock x:Name="TxtStatus" Text="{Binding Path=TxtStatus, Mode=OneWay}" Width="450" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Top" />
UPDATE 1
Setting value of TxtStatus in MqttService.cs
public class MqttService
{
private readonly ViewModel _viewModel;
public MqttService(ViewModel viewModel)
{
_viewModel = viewModel;
}
private void Log(string log)
{
_viewModel.TxtStatus = _viewModel.TxtStatus + log;
}
private void Connect()
{
_client.Connect(true);
Log(MsgConnected + _viewModel.TxtBrokerUrl + ", " + _viewModel.TxtClientId + "\n");
_viewModel.IsConnected = true;
}
MqttService Property in ViewModel.cs
private MqttService _mqttService;
public MqttService MqttService
{
get { return _mqttService ?? (_mqttService = new MqttService(this)); }
}
Now I wonder if maybe I have some kind of the circular reference problem (MqttService-ViewModel). I'm not sure, it looks good to me.
Your code works fine for me in WPF .NET4.0
so maybe your Property TxtStatus never get a string
_txtStatus ="new status"; // wrong
TxtStatus = "new status"; // right
or you get some interfering with your x:Name="TxtStatus" but that would be an Windows-Phone-7 based problem
Thank You all. After Erno de Weerd and ken2k wrote comments about threads I did some research and found this: Notify the UI Thread from Background Thread. I changed the way of setting value of the TxtStatus and it's working perfectly now. :)
(bad) Setting value of TxtStatus in MqttService.cs
private void Log(string log)
{
_viewModel.TxtStatus = _viewModel.TxtStatus + log;
}
(good) Setting value of TxtStatus in MqttService.cs
private void Log(string log)
{
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
App.ViewModel.TxtStatus = log + App.ViewModel.TxtStatus;
});
}