How to I handle keyboard appearance/disappearance in xamarin.forms - xamarin.forms

I have a topmost grid
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="120"/>
<RowDefinition Height="1"/>
<RowDefinition Height="5"/>
<RowDefinition Height="35"/>
<RowDefinition Height="5"/>
</Grid.RowDefinitions>
in my page.
How could I have * row to adjust it's height to respect the presence or absence of on screen keyboard?
So that the content in the row 0 shrinks as the keyboard appears.
Or, at the very least how could I detect keyboard shows up
on an Editor?
I have a custom renderer for that Editor already
so stuffing extra platform specific code can be done swiftly.

You can control Grid row size only manually. Editor.Focused and Editor.Unfocused is what you're looking for.
But you can combine it with event Triggers (http://developer.xamarin.com/guides/cross-platform/xamarin-forms/working-with/triggers/):
<EventTrigger Event="Focused">
<local:FocusedTriggerAction />
</EventTrigger>
public class FocusedTriggerAction : TriggerAction<Editor>
{
protected override void Invoke (Editor editor)
{
yourRow.Height = new GridLength(100);
}
}

For now I've adapted the solution found here
http://www.gooorack.com/2013/08/28/xamarin-moving-the-view-on-keyboard-show/
private UIView activeview; // Controller that activated the keyboard
/// <summary>
/// Initializes a new instance of the <see cref="ExtendedEditorRenderer"/> class.
/// </summary>
public ExtendedEditorRenderer ()
{
NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.DidShowNotification,KeyBoardUpNotification);
NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillHideNotification,KeyBoardDownNotification);
}
public void Dispose()
{
NSNotificationCenter.DefaultCenter.RemoveObserver(UIKeyboard.DidShowNotification);
NSNotificationCenter.DefaultCenter.RemoveObserver(UIKeyboard.WillHideNotification);
base.Dispose();
}
private void KeyBoardDownNotification(NSNotification notification)
{
try
{
var view = (ExtendedEditor)Element;
if (view.KeyboardListener != null)
{
Size s = new Size(0, 0);
view.KeyboardListener.keyboardSizeChangedTo(s);
}
}
catch (Exception ex)
{
//Debug.WriteLine("dcaught {0}", ex);
}
}
private void KeyBoardUpNotification(NSNotification notification)
{
try
{
// get the keyboard size
CGRect r = UIKeyboard.BoundsFromNotification(notification);
Size s = new Size(r.Size.Width, r.Size.Height);
var v = (ExtendedEditor)Element;
v.KeyboardListener.keyboardSizeChangedTo(s);
}
catch(Exception ex) {
//Debug.WriteLine("scaught {0}", ex);
}
}
shared platform "independent" code:
public interface IKeyboardListener
{
void keyboardSizeChangedTo(Size s);
}
public class ExtendedEditor : Editor ...
{

Related

Xamarin View freeze when raising OnPropertyChanged on value binded to Xamarin community toolkit BadgeView

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.

Uno Main Layout

I am new to Uno and I have been following the frame navigation tutorial. I noticed that when the frame navigates the entire window changes. This is good but not optimal. Is there a way in Uno to have a Main Layout, like you would see in a ASP.Net MVC project? I would rather not implement the navigation menu on each page.
To extend on #matfillion's answer, if NavigationView doesn't suit your needs, you can easily roll your own navigation shell whilst leveraging the built-in frame navigation. There's no requirement for Frame to be the top-level control in your application.
Here's an ultra-simple example, to illustrate the principle. The navigation list will stay visible at the top whilst navigating between pages.
Shell.xaml:
<UserControl x:Class="UnoTestbed44.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListView x:Name="NavigationList"
Background="LightGray"
ItemsSource="{x:Bind Pages}"
Grid.Row="0"
DisplayMemberPath="Label"
SelectionChanged="NavigationList_SelectionChanged">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
<Frame x:Name="MainFrame"
Grid.Row="1" />
</Grid>
</UserControl>
Shell.xaml.cs:
using System;
using System.Linq;
using Windows.UI.Xaml.Controls;
namespace UnoTestbed44
{
public sealed partial class Shell : UserControl
{
public NavigationItem[] Pages { get; } = new[] {
new NavigationItem {Label = "First page", PageType = typeof(Page1)},
new NavigationItem {Label = "Second page", PageType = typeof(Page2)},
};
public Shell()
{
this.InitializeComponent();
}
private void NavigationList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.FirstOrDefault() is NavigationItem navigationItem)
{
MainFrame.Navigate(navigationItem.PageType);
}
}
public class NavigationItem
{
public string Label { get; set; }
public Type PageType { get; set; }
}
}
}
Override OnLaunched() in App.xaml.cs:
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
#if NET5_0 && WINDOWS
_window = new Window();
_window.Activate();
#else
_window = Windows.UI.Xaml.Window.Current;
#endif
// Do not repeat app initialization when the Window already has content,
// just ensure that the window is active
if (_window.Content == null)
{
_window.Content = new Shell();
}
#if !(NET5_0 && WINDOWS)
if (e.PrelaunchActivated == false)
#endif
{
// Ensure the current window is active
_window.Activate();
}
}
While not exactly what you are looking for, I believe the NavigationView control is your closest bet. You can disable CompactView and obtain something like what is showcased in UnoGallery.

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