Xamarin View freeze when raising OnPropertyChanged on value binded to Xamarin community toolkit BadgeView - xamarin.forms

I'm currently struggling with a weird behavior concerning Xamarin Community Toolkit BadgeView component.
The component is used in the TitleView of my page like this:
<TabbedPage>
<Shell.TitleView>
<Grid ColumnDefinitions="6*,1*">
<Image Source="logo" HorizontalOptions="Center" Margin="0,2,0,2"/>
<StackLayout Grid.Column="1" Orientation="Horizontal">
<Label x:Name="For testing only" Text="{Binding NotificationsNumber}" VerticalOptions="Center"/>
<StackLayout VerticalOptions="Center" HorizontalOptions="EndAndExpand">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding OpenNotificationCommand}" NumberOfTapsRequired="1"/>
</StackLayout.GestureRecognizers>
<xct:BadgeView Text="{Binding NotificationsNumber}" BackgroundColor="#c1121f" TextColor="White" FontSize="Caption" AutoHide="True">
<Image>
<Image.Source>
<FontImageSource FontFamily="FASolid" Color="White" Size="Large" Glyph="{x:Static icons:FontAwesomeIcons.Bell}"/>
</Image.Source>
</Image>
</xct:BadgeView>
</StackLayout>
</StackLayout>
</Grid>
</Shell.TitleView>
Page content
</TabbedPage
For testing i added the label above with x:Name="for testing only" with Text Bindable Property binded to my property and the value update well without any concern.
In my ViewModel the property NotificationsNumber is initialized in the method InitializeAsync called by the constructor of the viewModel:
public class HomeViewModel : INotifyPropertyChanged, ApiViewModelBase
{
public event PropertyChangedEventHandler? PropertyChanged;
private int _notificationsNumber = 0;
public HomeViewModel(IApiClient client) : base(client)
{
OpenNotificationCommand = new Command(async () => await Shell.Current.GoToAsync($"{nameof(PlaceholderPage)}"));
InitializeAsync();
}
public ICommand OpenNotificationCommand { get; }
public int NotificationsNumber
{
get => _notificationsNumber;
private set => SetProperty(ref _notificationsNumber, value);
}
private async void InitializeAsync()
{
await RunInSafeScope(async () =>
{
// API call made with an instance of custom Http client instance
var notificationCountTask = HttpClient.GetWithRetryAsync<ValueResult<int>>(ApiRoutes.NOTIFICATION_COUNT);
var htmlSource = new HtmlWebViewSource();
await Task.WhenAll(notificationCountTask);
// notificationCountTask.Result.Value return 2 and update NotificationsNumber Property
NotificationsNumber = notificationCountTask.Result.Value;
}, (ex) =>
{
if (ex is ApiRequestException exception && exception.StatusCode == System.Net.HttpStatusCode.Unauthorized)
throw new Exception("Erreur", "Unauthorized");
else
throw new Exception("Erreur", "An internal error occured");
});
}
protected bool SetProperty<T>(ref T backingStore, T value, [CallerMemberName] string propertyName = "", Action? onChanged = null)
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected async Task RunInSafeScope(Func<Task> tryScope, Action<Exception> catchScope, Action? finallyScope = null)
{
try
{
await tryScope.Invoke();
}
catch (Exception ex)
{
catchScope.Invoke(ex);
}
finally
{
finallyScope?.Invoke();
}
}
}
For the sack of clarity i simplified the ViewModel and displayed only methods or properties or instructions usefull for this context.
So What is happening here is when i call the InitializeAsync the api call is made successfully then i set the value of NotificationsNumber property. The SetProperty method is raised, backing field is updated then OnPropertyChanged is invoked then i go back in the getter returning the updated value for finally having no response after that the screen remain freezed like if it was a deadlock.
I precise in the InitializeAsync() method i instantiate other properties with exactly the same process and there is no problems at all, that's why i think the problem is coming from the BindableProperty of the BadgeView component making an infinite loop or something of this kind.
I can't figure it out how to check if my assumptions are true, or test further.
Thanks in advance for your help!

Yes, it is the case as you said.
And I have created a new issue about this problem, you can follow it up here: https://github.com/xamarin/XamarinCommunityToolkit/issues/1900.
Thanks for your feedback and support for xamarin.
Best Regards.

Related

Fixed point button using absolute layout not responding to events

I have a listview on a content page. I have placed a fixed point button(think that is proper name) on the page using AvsoluteLayout. I have the button going to the top of the view using RaiseChild. The click event is not firing on the button but on the list view.
my on appearing where bubblebutton is the item at issue
protected override void OnAppearing()
{
base.OnAppearing();
IsBusy = true;
if(viewModel.PlayerActivities == null)
{
viewModel.LoadPlayerActivites.Execute(null);
}
IsBusy = false;
grid.RaiseChild(bubblebutton);
bubblebutton.Clicked += Bubblebutton_Clicked;
}
```
```
<AbsoluteLayout >
<ImageButton x:Name="bubblebutton"
BackgroundColor="Transparent"
Source="st_fab_button.png"
AbsoluteLayout.LayoutBounds="3,500,700,80"
Clicked="bubblebutton_Clicked"
IsEnabled="True"
/>
</AbsoluteLayout>
```
[button click is not happening][1]
Thank you for any help sorry for poor clip art skill
[1]: https://i.stack.imgur.com/QJixO.png
Welcome to SO !
If using Button inside Item of ListView , should use the MVVM architecture to do . That means we should use Binding model to get the click event .
Such as modified code as follow :
```
<AbsoluteLayout >
<ImageButton x:Name="bubblebutton"
BackgroundColor="Transparent"
Source="st_fab_button.png"
AbsoluteLayout.LayoutBounds="3,500,700,80"
Command="{Binding MyCommand}"
IsEnabled="True"
/>
</AbsoluteLayout>
```
Then in your ViewModle should declare the MyCommand :
public ICommand MyCommand { private set; get; }
public ViewModel()
{
MyCommand = new Command(
execute: () =>
{
// do some thing
RefreshCanExecutes();
},
canExecute: () =>
{
// return !IsEditing;
});
ยทยทยท
}
In addition , you also can pass Parameters , more info can have a look at this document .

An emptyView for loading data and another for when there is no data available

I have a case of using a CarouselView that is displayed based on certain data brought from an API, the point is that I need to see a certain view or at least text while the API data is being downloaded and another one in case That there is no data.
I tried to get to this using RefreshView and EmptyView but I cannot achieve the required behavior, I can make an EmptyView appear immediately the data begins to load since at that moment the ItemSource is null, then when the data reaches the app the Carousel appears , which seems to me quite ugly, the ideal would be to show some view that next to the RefreshView indicator shows that the data is loading and then in case of not bringing any data show a view that of the feedback that API data did not return .
I hope I have made myself understood and I hope someone can give me an idea on how to achieve this behavior.
MyViewModel:
public MyViewModel()
{
IsRefreshing = true;
Things = new ObservableCollection<Things>();
var t = Task.Run(async () =>
{
await LoadThings();
});
Task.WhenAll(t);
IsRefreshing = false;
}
private async Task LoadThings()
{
Things = new List<Thing>(await App.WebApiManager.GetThingsAsync(Id));
}
My IsRefreshing property is linked to the IsRefreshing property in the RefreshView that encompasses my CarouselView
I think you could use two empty view and switch between them when the refreshing status changes, and here is the code:
add two content view in in XAML and set default empty view to LoadingData:
<ContentPage.Resources>
<ContentView x:Key="LoadingData">
<StackLayout>
<Label Text="Loading data..."
Margin="10,25,10,10"
FontAttributes="Bold"
FontSize="18"
HorizontalOptions="Fill"
HorizontalTextAlignment="Center" />
</StackLayout>
</ContentView>
<ContentView x:Key="NoDataLoaded">
<StackLayout>
<Label Text="No items to display."
Margin="10,25,10,10"
FontAttributes="Bold"
FontSize="18"
HorizontalOptions="Fill"
HorizontalTextAlignment="Center" />
</StackLayout>
</ContentView>
</ContentPage.Resources>
<StackLayout Margin="20">
<RefreshView IsRefreshing="{Binding IsRefreshing}"
Command="{Binding RefreshCommand}">
<CarouselView x:Name="carouselView"
EmptyView="{StaticResource LoadingData}">
... ...
and in code, show different empty view accordingly:
public partial class HorizontalPullToRefreshPage : ContentPage
{
AnimalsViewModel viewModel;
public HorizontalPullToRefreshPage()
{
InitializeComponent();
viewModel = new AnimalsViewModel();
this.BindingContext = viewModel;
viewModel.PropertyChanged += ViewModel_PropertyChanged;
}
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName.Equals("IsRefreshing"))
{
if (viewModel.IsRefreshing && viewModel.Animals.Count==0)
{
carouselView.EmptyView = Resources["LoadingData"];
}
else if (!viewModel.IsRefreshing && viewModel.Animals.Count == 0)
{
carouselView.EmptyView = Resources["NoDataLoaded"];
}
}
}
protected override async void OnAppearing()
{
base.OnAppearing();
await Task.Delay(2000);
carouselView.ItemsSource = viewModel.Animals;
}
}
then, every time the property IsRefreshing changed, you got a chance to switch the empty view.
Hope it helps.

Xamarin Forms -> Activity Indicator not working if Commands of statements to be executed

Using Visual Studio 2017 Community 15.8.1
This is after going through all options of stackoverflow regarding ActivityIndicator. So though it may be a duplication but nothing is helping me out.
So finally decided to post my workouts and get best help from here.
What I have tried till now:-
1. {Binding IsLoading} + INotifyPropertyChanged + public void RaisePropertyChanged(string propName) + IsLoading = true; concept.
2. ActivityIndicator_Busy.IsVisible = false; (Direct control accessed)
These two approaches were mostly recommended and I went into depth of each since lot of hours in last few weeks. But nothing got crack.
What I achieved?:-
ActivityIndicator_Busy.IsVisible = false; concept is working smooth only when I put return before executing the statements (for testing purpose); statement on Button Clicked event. (Attached Image)
But as soon as I remove the return; On Pressing Button, directly after some pause, the HomePage Opens.
MY Questions:-
1. This is particular with the current scenario how to get the ActivityIndicator run Immediately when user clicks the Welcome Button.
2. Pertaining to same, When app starts there is also a blank white screen coming for few seconds almost 30 seconds which I also I want to show ActivityIndicator. But dont know how to impose that logic at which instance.
My Inputs
My MainPage.xaml File:-
(Edited 06-Sept-2018 09.11 pm)
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage x:Name="page_main_page"
NavigationPage.HasBackButton="False"
NavigationPage.HasNavigationBar="False"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:appNutri"
BindingContext="{x:Reference page_main_page}"
x:Class="appNutri.MainPage">
<ContentPage.Content>
<StackLayout BackgroundColor="White"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
<StackLayout>
<Image x:Name="Image_Welcome"
Source="welcome.png"
HorizontalOptions="CenterAndExpand"
VerticalOptions="CenterAndExpand"
WidthRequest="300"
HeightRequest="300" />
<Button x:Name="Button_Welcome"
Clicked="Button_Welcome_Clicked"
Text="Welcome!"
BackgroundColor="DeepSkyBlue"
HorizontalOptions="CenterAndExpand"
VerticalOptions="CenterAndExpand"
TextColor="White"
HeightRequest="60" />
</StackLayout>
<StackLayout BackgroundColor="White"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
<ActivityIndicator
x:Name="ActivityIndicator_Busy"
Color="Black"
IsEnabled="True"
HorizontalOptions="Center"
VerticalOptions="Center"
IsRunning="{Binding Source={x:Reference page_main_page}, Path=IsLoading}"
IsVisible="{Binding Source={x:Reference page_main_page}, Path=IsLoading}" />
</StackLayout>
</StackLayout>
</ContentPage.Content>
</ContentPage>
My MainPage.cs Code:-
(Edited on 06-Sept-2018 09.13 pm)
using appNutri.Model;
using SQLite;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace appNutri
{
public partial class MainPage : Xamarin.Forms.ContentPage, INotifyPropertyChanged
{
private bool isLoading;
public bool IsLoading
{
get
{
return isLoading;
}
set
{
isLoading = value;
RaisePropertyChanged("IsLoading");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
public MainPage()
{
InitializeComponent();
BindingContext = this;
}
protected override void OnAppearing()
{
base.OnAppearing();
BindingContext = this;
}
protected async void Button_Welcome_Clicked(object sender, EventArgs e)
{
IsLoading = true;
await Select_Local_User_Information();
IsLoading = false;
}
private async Task Select_Local_User_Information()
{
IsLoading = true;
string where_clause = "";
try
{
Sql_Common.Database_Folder_Path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
string Database_Full_Path = Path.Combine(Sql_Common.Database_Folder_Path, Sql_Common.Database_Name);
SQLiteConnection connection = new SQLiteConnection(Database_Full_Path);
//connection.DropTable<User_Master>();
//connection.Delete(connection.Table<User_Master>());
//connection.CreateTable<User_Master>(CreateFlags.ImplicitPK | CreateFlags.AutoIncPK);
connection.CreateTable<User_Master>();
int count = connection.ExecuteScalar<int>("Select count(*) from User_Master");
if (count == 0)
{
connection.DropTable<User_Master>();
connection.CreateTable<User_Master>();
//IsLoading = false;
//IsBusy = false;
await Navigation.PushAsync(new User_Register_Page());
}
else
{
Sql_Common.User_Logged = true;
var Local_User_Data = connection.Table<User_Master>().ToList();
User_Master.Logged_User_Details_Container.First_Name = Local_User_Data[0].First_Name;
User_Master.Logged_User_Details_Container.Cell1 = Local_User_Data[0].Cell1;
where_clause = " Upper ( First_Name ) = " + "'" + User_Master.Logged_User_Details_Container.First_Name.ToUpper().Trim() + "'" + " and " +
" Cell1 = " + "'" + User_Master.Logged_User_Details_Container.Cell1.Trim() + "'";
int records = Sql_Common.Get_Number_Of_Rows_Count("User_Master", where_clause);
if (records == 0)
{
connection.DropTable<User_Master>();
connection.CreateTable<User_Master>();
IsLoading = false;
await Navigation.PushAsync(new User_Register_Page());
}
else
{
User_Master.User_Master_Table(where_clause, User_Master.Logged_User_Details_Container);
IsLoading = false;
await Navigation.PushAsync(new User_Home_Page());
}
}
connection.Close();
}
catch (SQLiteException ex)
{
string ex_msg = ex.Message;
}
IsLoading = false;
}
}
}
04-Oct-2018
Finally resolved with This Answer
Update 2018-09-10
You think that you have implemented INotifyPropertyChanged by adding INotifyPropertyChanged to your class definition and adding the event
public event PropertyChangedEventHandler PropertyChanged;
along with its event invocator
public void RaisePropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
Anyway, since ContentPage already implements INotifyPropertyChanged, adding those did not implement INotifyPropertyChanged. ContentPage already defines the event (or rather BindableObjectfrom which ContentPage indirectly inherits). Any object that relies on being informed about property changes in your page will subscribe to the PropertyChanged event of the ancestor and not the PropertyChanged event you defined, hence the ActivityIndicator will not update.
Just remove the event you defined and call OnPropertyChanged instead of RaisePropertyChanged() and you should be fine.
private bool isLoading;
public bool IsLoading
{
get
{
return isLoading;
}
set
{
isLoading = value;
OnPropertyChanged();
}
}
Since OnPropertyChanged is declared as
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
you don't have to pass the property name by hand. The compiler will do that for you beacsue of the CallerMemberNameAttribute.
End Update
The XAML extension {Binding IsLoading} binds the ActivityIndicator to the BindingContext of your page. By default the BindingContext is null, hence there is nothing to bind to and all your efforts are to no avail.
With a viewmodel
The preferred solution would be to use a viewmodel and assign it to MainPage.BindingContext, e.g.
var page = new MainPage()
{
BindingContext = new MainPageViewModel()
}
but if you take that road, you should move all of your UI logic to that viewmodel and encapsulate your SQL access and business logic in other classes, to keep the viewmodel clean from resource accesses and business logic. Having the resource accesses and logic in code behind may work for that small example, but is likely to become an unmaintainable mess.
Without a viewmodel
Anyway, you don't have to use a viewmodel to use bindings. You can set the BindingContext for the page (or some children) or use the Source of the BindingExtension
Setting the BindingContext
The BindingContext is passed from any page or view to it's children. You first have to give your page a name with x:Name="Page" (don't have to use Page, anyway, you can't use the class name of your page) and set the BindingContext to that page
<ContentPage ...
x:Name="Page"
BindingContext="{x:Reference Page}"
...>
now binding to IsLoading should work.
Using Source in the Binding
If you want to reference something else than the BindingContext of a view, BindingExtension has a property Source. You have to give a name to your page, too (see above)
<ContentPage ...
x:Name="Page"
...>
and now you can reference this in your binding
<ActivityIndicator
...
IsRunning="{Binding Path=IsLoading, Source={x:Reference Page}}"
IsVisible="{Binding Path=IsLoading, Source={x:Reference Page}}"/>

UWP Change loading state using binding and async functions

I am coming from an Angular 2 and a C# back end background, so for the Angular side of things I am used to working with async functions and code, as well the C# background I understand the base libraries.
I am trying to create a simple page that has a a button, and a loading gif. You click the button the loading gif appears, 10 seconds later it disappears.
I can make the loading start no problem, but the nature of the async code jumps the execution and instantly makes the gif disappear.
How do I go about starting the spinner / making a gif visible, waiting 10 seconds in a non ui-blocking manner, and then finish with a thread-safe way of ending the animation / gif visibility?
View-Model code:
public class LoadingViewModel: INotifyPropertyChanged
{
private Visibility _loadingState;
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public LoadingViewModel()
{
this._loadingState = Visibility.Collapsed;
}
public Visibility LoadingState
{
get {
return this._loadingState;
}
set {
this._loadingState = value;
this.OnPropertyChanged();
}
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
// Raise the PropertyChanged event, passing the name of the property whose value has changed.
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
MainView.xaml.cs:
public LoadingViewModel LoadingViewModel { get; set; }
public MainPage()
{
this.InitializeComponent();
this.LoadingViewModel = new LoadingViewModel();
}
private async Task BeginLoading()
{
LoadingViewModel.LoadingState = Visibility.Visible;
await Task.Factory.StartNew(() =>
{
Task.Delay(TimeSpan.FromSeconds(10));
}).ContinueWith(EndLoadingState);
}
//Updated and works but is there a better way?
private async Task BeginLoading()
{
LoadingViewModel.LoadingState = Visibility.Visible;
await Task.Factory.StartNew(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(10));
await EndLoadingState(); //<-- New EndLoadingState doesn't accept parms
});
}
private async void EndLoadingState(object state)
{
await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => {
LoadingViewModel.LoadingState = Visibility.Collapsed;
});
}
private async void Button_Click(object sender, RoutedEventArgs e)
{
await BeginLoading();
}
And lastly a basic stack panel with my button and image:
<StackPanel Margin="10,144,0,144">
<Button Content="Begin Loading for 10 seconds" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0" Height="157" Width="366" FontSize="22" Background="{x:Null}" BorderThickness="5" BorderBrush="#FF58FF00" Click="Button_Click"/>
<Image HorizontalAlignment="Center" Height="250" VerticalAlignment="Center" Width="250" Margin="0,25,0,0" Stretch="UniformToFill" Source="Assets/LoadingBubbles.gif" Visibility="{x:Bind Path=LoadingViewModel.LoadingState, Mode=TwoWay}"/>
</StackPanel>
First, try using a bool property in your LoadingViewModel instead of Visibility as the latter is a UI attribute. You generally don't want that in your ViewModel. If your target version of Windows 10 is 14393 or higher, you can bind it directly without a BoolToVisibilityConverter. And the binding doesn't need to be TwoWay also.
Visibility="{x:Bind Path=LoadingViewModel.IsLoading, Mode=OneWay}"
Second, XAML binding will actually take care of dispatching the updated value onto the UI thread. So you can also get rid of Dispatcher.RunAsync and have a normal void method
private void EndLoadingState(object state)
{
LoadingViewModel.IsLoading = false;
}
Finally, your BeginLoading method(best to rename it to BeginLoadingAsync) can be simplified to
private async Task BeginLoadingAsync()
{
LoadingViewModel.IsLoading = true;
await Task.Delay(TimeSpan.FromSeconds(10));
EndLoadingState();
}
Hope this helps!

RelayCommand.CanExecute not updating IsEnabled in UI

I have a Windows Phone 8 app and I have a RelayCommand Instance called DiscoverExpansionModulesCommand. I have a button with the Command property bound to DiscoverExpansionModulesCommand. When the app first loads, the button is enabled or disabled properly. However, when on the page and I want to change whether the command can execute, the method CanExecuteDiscoverExpansionModulesCommand() properly fires and it returns the proper true or false value, but the button does not reflect it. Why isn't button updating it's UI? I found another article on this issue here http://social.msdn.microsoft.com/Forums/en-US/silverlightarchieve/thread/48a341e4-f512-4c33-befd-b614404b4920/
My ViewModel:
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using GalaSoft.MvvmLight.Messaging;
using MAL.Portable.Commands;
using MAL.Portable.Message;
using MAL.Portable.Model;
using System;
using System.Collections.Generic;
using System.Windows.Input;
namespace MAL.Portable.ViewModel
{
public class SettingsViewModel : ViewModelBase
{
// Define an observable collection property that controls can bind to.
private List<Setting> settings;
private String controllerUrl;
private String controllerPort;
private String temperature;
private Wifi wifi;
private Boolean connected;
private Boolean checkingConnection;
public SettingsViewModel()
{
DiscoverExpansionModulesCommand = new RelayCommand(OnDiscoverExpansionModules, CanExecuteDiscoverExpansionModulesCommand);
Messenger.Default.Register<RetrieveSettingsMessage>
(
this, (action) => RetrievedListsMessage(action)
);
Messenger.Default.Send<GetSettingsMessage>(new GetSettingsMessage());
}
public ICommand DiscoverExpansionModulesCommand
{
get;
private set;
}
public String ConnectionStatus
{
get
{
if (checkingConnection)
return "checking";
else
return connected ? "connected" : "not connnected";
}
}
private Boolean CanExecuteDiscoverExpansionModulesCommand()
{
return connected;
}
private void OnDiscoverExpansionModules()
{
}
private void CheckConnection()
{
wifi = null;
if (!String.IsNullOrWhiteSpace(ControllerUrl) && !String.IsNullOrWhiteSpace(ControllerPort) && !checkingConnection)
{
checkingConnection = true;
wifi = new ReefAngelWifi(controllerUrl, controllerPort);
wifi.TestConnectionComplete += wifi_TestConnectionComplete;
wifi.RequestFail += wifi_RequestFail;
wifi.BeginTestConnection();
}
}
private void wifi_RequestFail(object sender, RequestExceptionEventArgs e)
{
connected = false;
checkingConnection = false;
RaisePropertyChanged("ConnectionStatus");
}
private void wifi_TestConnectionComplete(object sender, TestConnectionEventArgs e)
{
connected = e.TestSuccessful;
checkingConnection = false;
DiscoverExpansionModulesCommand.CanExecute(null);
RaisePropertyChanged("ConnectionStatus");
RaisePropertyChanged("DiscoverExpansionModulesCommand");
}
private object RetrievedListsMessage(RetrieveSettingsMessage action)
{
settings = action.Settings;
CheckConnection();
return null;
}
private String GetStringValue(String key)
{
if (settings == null) return String.Empty;
var item = settings.Find(x => x.Key == key);
if (item == null) return String.Empty;
else return item.Value;
}
private Boolean GetBooleanValue(String key)
{
if (settings == null) return false;
var item = settings.Find(x => x.Key == key);
if (item == null) return false;
else return Boolean.Parse(item.Value);
}
}
}
And the XAML
<phone:PhoneApplicationPage
xmlns:ReefAngel="clr-namespace:MAL.WindowsPhone8"
xmlns:Controls="clr-namespace:Telerik.Windows.Controls;assembly=Telerik.Windows.Controls.Input"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Platform.WP8"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:telerikPrimitives="clr-namespace:Telerik.Windows.Controls;assembly=Telerik.Windows.Controls.Primitives"
x:Class="MAL.WindowsPhone8.ReefAngel.SettingsPage"
xmlns:converter="clr-namespace:MAL.WindowsPhone8.Converters"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
DataContext="{Binding Settings, Source={StaticResource Locator}}"
SupportedOrientations="Portrait" Orientation="Portrait"
mc:Ignorable="d"
shell:SystemTray.IsVisible="True">
<phone:PhoneApplicationPage.Resources>
<converter:BooleanToStringConverter x:Key="temperatureConverter" TrueString="Celsius" FalseString="Fahrenheit" />
<converter:BooleanToStringConverter x:Key="timeFormatConverter" TrueString="24 hour" FalseString="12 hour" />
<converter:BooleanToStringConverter x:Key="dateFormatConverter" TrueString="dd/mm/yyyy" FalseString="mm/dd/yyyy" />
</phone:PhoneApplicationPage.Resources>
<!--LayoutRoot is the root grid where all page content is placed-->
<Grid x:Name="LayoutRoot" >
<phone:Pivot Title="{Binding LocalizedResources.ApplicationTitle, Source={StaticResource LocalizedStrings}, StringFormat='\{0\} Settings'}">
<phone:PivotItem Header="connection">
<Grid>
<StackPanel Margin="12,0,0,0">
<TextBlock Margin="0,20,0,0" TextWrapping="Wrap" Text="Reef Angel Wifi Address"/>
<TextBox Height="72" TextWrapping="Wrap" Text="{Binding ControllerUrl, Mode=TwoWay}"/>
<TextBlock Margin="0,20,0,0" TextWrapping="Wrap" Text="Reef Angel Wifi Port"/>
<TextBox Height="72" TextWrapping="Wrap" Text="{Binding ControllerPort, Mode=TwoWay}"/>
<StackPanel Orientation="Horizontal">
<TextBlock Margin="0,20,0,0" TextWrapping="Wrap" Text="Reef Angel Wifi Status : "/>
<TextBlock Margin="0,20,0,0" TextWrapping="Wrap" Text="{Binding ConnectionStatus, Mode=OneWay}"/>
</StackPanel>
</StackPanel>
</Grid>
</phone:PivotItem>
<phone:PivotItem Header="expansion">
<Grid>
<Button Content="Discover Expansion Modules" x:Name="DiscoverButton" Command="{Binding DiscoverExpansionModulesCommand, Mode=OneWay}" />
</Grid>
</phone:PivotItem>
</phone:Pivot>
</Grid>
</phone:PhoneApplicationPage>
I am using the MVVM Light Portable Class Libraries.
You need to call RelayCommand.RaiseCanExecuteChanged() when the conditions you evaluate inside your CanExecute method change.
Edit
private void wifi_RequestFail(object sender, RequestExceptionEventArgs e)
{
connected = false;
checkingConnection = false;
RaisePropertyChanged("ConnectionStatus");
DiscoverExpansionModulesCommand.RaiseCanExecuteChanged();
}
private void wifi_TestConnectionComplete(object sender, TestConnectionEventArgs e)
{
connected = e.TestSuccessful;
checkingConnection = false;
DiscoverExpansionModulesCommand.CanExecute(null);
RaisePropertyChanged("ConnectionStatus");
RaisePropertyChanged("DiscoverExpansionModulesCommand");
DiscoverExpansionModulesCommand.RaiseCanExecuteChanged();
}
This will not cause a loop as it only tells the RelayCommand to re-execute the specified CanExecute method. In your case this only means that the property CanExecuteDiscoverExpansionModulesCommand is read.
It appears to be a cross threading issue. And figuring out how to call a Dispatcher in the PCL was tricky, but I found it here: Update UI thread from portable class library

Resources