Need a custom picker in which the Title reduces in size once an Item is selected from the Picker as in image. Same happens for a MaterialisedEntry but need the same for Picker Control
Xamarin.Forms does not allow you to inject a custom view (like a ContentView with a stacklayout) into the Picker control). What you could do is to use a Grid where you have a stacklayout overlaid on a Picker. Based on the SelectedItem property of the picker you would need to update the Text of the State Label.
<Grid HorizontalOptions="Center" VerticalOptions="Center">
<Picker x:Name="picker"
Title="select a state">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>Colorado</x:String>
<x:String>California</x:String>
<x:String>Ohio</x:String>
</x:Array>
</Picker.ItemsSource>
</Picker>
<StackLayout WidthRequest="300" InputTransparent="True" BackgroundColor="White">
<Label Text="State" FontSize="10"/>
<!--Here you would need to bind the Label Text to a property
that changes according to the SelectedItem of the picker-->
<Label Text="Colorado" FontSize="14"/>
</StackLayout>
</Grid>
According to your description, I suggest you can use custom render to add arrow for Picker control, like this:
Create class name CustomPicker in PLC.
public class CustomPicker : Picker
{
public static readonly BindableProperty ImageProperty =
BindableProperty.Create(nameof(Image), typeof(string), typeof(CustomPicker), string.Empty);
public string Image
{
get { return (string)GetValue(ImageProperty); }
set { SetValue(ImageProperty, value); }
}
}
Create class name CustomPickerRenderer in Android or ios.
[assembly: ExportRenderer(typeof(CustomPicker), typeof(CustomPickerRenderer))]
namespace demo3.Droid
{
public class CustomPickerRenderer : PickerRenderer
{
CustomPicker element;
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
element = (CustomPicker)this.Element;
if (Control != null && this.Element != null && !string.IsNullOrEmpty(element.Image))
{
Control.Background = AddPickerStyles(element.Image);
}
}
public LayerDrawable AddPickerStyles(string imagePath)
{
//ShapeDrawable border = new ShapeDrawable();
// border.Paint.Color = Android.Graphics.Color.Gray;
// border.SetPadding(10,10,10,10);
// border.Paint.SetStyle(Paint.Style.Stroke);
//Drawable[] layers = { border , GetDrawable(imagePath) };
Drawable[] layers = { GetDrawable(imagePath) };
LayerDrawable layerDrawable = new LayerDrawable(layers);
layerDrawable.SetLayerInset(0, 0, 0, 0, 0);
return layerDrawable;
}
private BitmapDrawable GetDrawable(string imagePath)
{
int resID = Resources.GetIdentifier(imagePath, "drawable", this.Context.PackageName);
var drawable = ContextCompat.GetDrawable(this.Context, resID);
var bitmap = ((BitmapDrawable)drawable).Bitmap;
var result = new BitmapDrawable(Resources, Bitmap.CreateScaledBitmap(bitmap, 70, 70, true));
result.Gravity = Android.Views.GravityFlags.Right;
return result;
}
}
}
3.Reference this customPicker in PLC.
<Frame
Padding="8"
BorderColor="Gray"
CornerRadius="20"
HasShadow="True"
IsClippedToBounds="True">
<StackLayout>
<Label
x:Name="label"
FontSize="20"
Text="state" />
<picker:CustomPicker
x:Name="picker1"
Title="select one item"
Image="ic_arrow_drop_down"
SelectedIndexChanged="Picker1_SelectedIndexChanged">
<picker:CustomPicker.Items>
<x:String>1</x:String>
<x:String>2</x:String>
</picker:CustomPicker.Items>
</picker:CustomPicker>
</StackLayout>
</Frame>
private void Picker1_SelectedIndexChanged(object sender, EventArgs e)
{
var picker = sender as CustomPicker;
if(picker.SelectedIndex>-1)
{
label.FontSize = 10;
}
}
This is the screenshot:
Related
I am sorry to be asking what seems really obvious question but I've been unable to set the properties (e.g. background color) of the list view / contents when using the picker
The Picker properties style what you see before you expand the list to select contents, but I cannot see or find how to affect the latter
In this example from my XAML the collapsed view of the Picker is styled correctly, but when it opens the background is white / transparent
Sorry, I have looked in many links and just can't find the info
<Picker
VerticalOptions="CenterAndExpand"
Grid.Column="1"
Grid.Row="1"
Title="PICKER"
BackgroundColor="Transparent"
TitleColor="White"
FontSize="Medium"
Style="{StaticResource AlphabetPicker}"
x:Name="AlphabetPicker"
ItemsSource="{Binding Alphabet}"
SelectedIndexChanged="GetLetterSelected"
HorizontalOptions="Start">
You could use custom renderer.
I follow the code in the link: Customize the Xamarin.Forms Picker Popup List
MyPicker.cs
public class MyPicker : Xamarin.Forms.Picker
{
}
MyPickerRenderer.cs
[assembly: ExportRenderer(typeof(MyPicker), typeof(MyPickerRenderer))]
namespace XamarinDemo.Droid.Renderer
{
class MyPickerRenderer : PickerRenderer
{
IElementController ElementController => Element as IElementController;
public MyPickerRenderer(Context context) : base(context)
{
}
private AlertDialog _dialog;
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Picker> e)
{
base.OnElementChanged(e);
if (e.NewElement == null || e.OldElement != null)
return;
Control.Click += Control_Click;
}
protected override void Dispose(bool disposing)
{
Control.Click -= Control_Click;
base.Dispose(disposing);
}
private void Control_Click(object sender, EventArgs e)
{
Xamarin.Forms.Picker model = Element;
var picker = new NumberPicker(Context);
if (model.Items != null && model.Items.Any())
{
// set style here
picker.MaxValue = model.Items.Count - 1;
picker.MinValue = 0;
picker.SetBackgroundColor(Android.Graphics.Color.Yellow);
picker.SetDisplayedValues(model.Items.ToArray());
picker.WrapSelectorWheel = false;
picker.Value = model.SelectedIndex;
}
var layout = new LinearLayout(Context) { Orientation = Orientation.Vertical };
layout.AddView(picker);
ElementController.SetValueFromRenderer(VisualElement.IsFocusedProperty, true);
var builder = new AlertDialog.Builder(Context);
builder.SetView(layout);
builder.SetTitle(model.Title ?? "");
builder.SetNegativeButton("Cancel ", (s, a) =>
{
ElementController.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
// It is possible for the Content of the Page to be changed when Focus is changed.
// In this case, we'll lose our Control.
Control?.ClearFocus();
_dialog = null;
});
builder.SetPositiveButton("Ok ", (s, a) =>
{
ElementController.SetValueFromRenderer(Xamarin.Forms.Picker.SelectedIndexProperty, picker.Value);
// It is possible for the Content of the Page to be changed on SelectedIndexChanged.
// In this case, the Element & Control will no longer exist.
if (Element != null)
{
if (model.Items.Count > 0 && Element.SelectedIndex >= 0)
Control.Text = model.Items[Element.SelectedIndex];
ElementController.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
// It is also possible for the Content of the Page to be changed when Focus is changed.
// In this case, we'll lose our Control.
Control?.ClearFocus();
}
_dialog = null;
});
_dialog = builder.Create();
_dialog.DismissEvent += (ssender, args) =>
{
ElementController?.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
};
_dialog.Show();
}
}
}
Xaml:
<StackLayout>
<local:MyPicker x:Name="picker"
Title="Select a monkey"
TitleColor="Red">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>Baboon</x:String>
<x:String>Capuchin Monkey</x:String>
<x:String>Blue Monkey</x:String>
<x:String>Squirrel Monkey</x:String>
<x:String>Golden Lion Tamarin</x:String>
<x:String>Howler Monkey</x:String>
<x:String>Japanese Macaque</x:String>
</x:Array>
</Picker.ItemsSource>
</local:MyPicker>
</StackLayout>
I have a ListView and I'm using the standard "native" template and groups.
How do I set the font for the text in the group header and the item?
Here is an example for setting dynamic LayoutOptions in ListHeader, like wise you can pre-set these attributes for individual label and customize your header. now as what you want, you can set your own font family or colour or font size, or may be customize your header cell, It is just an ItemTemplate
<ListView x:Name="ListView" IsGroupingEnabled="true">
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell>
<Label
HorizontalOptions="{Binding Optn}"
Text="{Binding Heading}"
VerticalOptions="{Binding Optn}" />
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding DisplayName}" />
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Happy Coding!
I've fixed the text and detail labels using this...
public class MyTextCellRenderer : TextCellRenderer
{
public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
{
var cell = base.GetCell(item, reusableCell, tv);
if (cell.TextLabel.Font.FamilyName != CommonStyles.RegularFontName)
{
cell.TextLabel.Font = UIFont.FromName(
CommonStyles.RegularFontName, cell.TextLabel.Font.PointSize);
cell.DetailTextLabel.Font = UIFont.FromName(
CommonStyles.RegularFontName, cell.DetailTextLabel.Font.PointSize);
}
return cell;
}
}
For the section headers, this works initially...
UILabel.AppearanceWhenContainedIn(typeof(UITableView)).Font = ...
When the section header has been scrolled off screen and back on again, it reverts to the standard though.
But this works...
public class CustomTableViewRenderer : TableViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<TableView> e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
Control.Source = new CustomTableViewModelRenderer(e.NewElement);
}
}
}
public class CustomTableViewModelRenderer : UnEvenTableViewModelRenderer
{
public CustomTableViewModelRenderer(TableView model) : base(model)
{
}
public override void WillDisplayHeaderView(UITableView tableView, UIView headerView, nint section)
{
if (headerView is UITableViewHeaderFooterView view)
{
view.TextLabel.Font = UIFont.FromName(CommonStyles.RegularFontName, 18);
}
}
}
That's a lot of code to change some fonts.
Hello guys :D I am having a problem with the android part of the Xamarin.Forms
When I Navigate from a AbsoluteLayout with a Map and TabbleView (enter image description here) to a Grid with only a Map, the Map from the previous page stays static on top of the second one(enter image description here). This problem does not manifest in iOS, only in Android. If any of you guys know the problem please tell me so I can quickly fix :D
Page with First Map
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="SGPI.Intervencao.CriarIntervencao"
xmlns:local="clr-namespace:SGPI.Shared"
xmlns:esriUI="clr-namespace:Esri.ArcGISRuntime.Xamarin.Forms;assembly=Esri.ArcGISRuntime.Xamarin.Forms"
Padding="5,5">
<ContentPage.Resources>
<ResourceDictionary>
<local:MapViewModel x:Key="MapViewModel" />
</ResourceDictionary>
</ContentPage.Resources>
<AbsoluteLayout>
<esriUI:MapView Map="{Binding Map, Source={StaticResource MapViewModel}}" x:Name="map" AbsoluteLayout.LayoutBounds="0,0, 1, 0.6" AbsoluteLayout.LayoutFlags="All"/>
<TableView Intent="Form" HasUnevenRows="True" AbsoluteLayout.LayoutBounds="0,1, 1, 0.4" AbsoluteLayout.LayoutFlags="All">
<TableRoot>
<TableSection Title="Information">
<EntryCell Label="Nome" Text="{Binding Name}" Placeholder="Nome"/>
<EntryCell Label="Codigo" Text="{Binding Code}" Placeholder="Codigo"/>
<ViewCell>
<StackLayout Orientation="Horizontal">
<Label Text="Status" HorizontalOptions="Start" VerticalOptions="Center"/>
<Picker x:Name="pick" SelectedIndex="{Binding Index}" SelectedItem="{Binding Status}" Title="Status" HorizontalOptions="FillAndExpand">
<Picker.Items>
<x:String>Em Construção</x:String>
<x:String>Construido</x:String>
</Picker.Items>
</Picker>
</StackLayout>
</ViewCell>
<ViewCell>
<Button Image="editMap.png" Clicked="Button_Clicked" />
</ViewCell>
</TableSection>
</TableRoot>
</TableView>
</AbsoluteLayout>
<ContentPage.ToolbarItems>
<ToolbarItem Icon="dan.png" Order="Primary" x:Name="done" Clicked="Done"/>
</ContentPage.ToolbarItems>
Code Behind
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace SGPI.Intervencao
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class CriarIntervencao : ContentPage
{
public event EventHandler<IntervencaoClass> IntervencaoAdded;
public event EventHandler<IntervencaoClass> IntervencaoUpdated;
public CriarIntervencao(IntervencaoClass intervencao)
{
if(intervencao == null)
throw new ArgumentNullException(nameof(intervencao));
InitializeComponent();
BindingContext = new IntervencaoClass
{
Code = intervencao.Code,
Name = intervencao.Name,
Status = intervencao.Status,
Index = intervencao.Index,
Id = intervencao.Id,
Polygons = intervencao.Polygons
};
if(intervencao.Polygons != null)
map.GraphicsOverlays.Add(intervencao.Polygons);
}
private async void Done(object sender, EventArgs e)
{
var intervencao = BindingContext as IntervencaoClass;
if (filled())
{
await DisplayAlert("Erro", "Preenche tudo", "OK");
return;
}
map.GraphicsOverlays.Clear();
if (!intervencao.Id.HasValue)
{
intervencao.Id = 1;
IntervencaoAdded?.Invoke(this, intervencao);
}
else
{
IntervencaoUpdated?.Invoke(this, intervencao);
}
await Navigation.PopAsync();
}
public bool filled()
{
var intervencao = BindingContext as IntervencaoClass;
return String.IsNullOrEmpty(intervencao.Name) || String.IsNullOrEmpty(intervencao.Code) || pick.SelectedIndex == -1;
}
private async void Button_Clicked(object sender, EventArgs e)
{
map.GraphicsOverlays.Clear();
var page = new MapPages.MapPage((BindingContext as IntervencaoClass).Polygons);
page.AcceptedMap += (send, graphics) => {
var intervencao = BindingContext as IntervencaoClass;
intervencao.Polygons = graphics;
map.GraphicsOverlays.Add(graphics);
Navigation.PopAsync();
};
await Navigation.PushAsync(page);
}
}
}
Second Page
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:SGPI.Shared"
xmlns:esriUI="clr-namespace:Esri.ArcGISRuntime.Xamarin.Forms;assembly=Esri.ArcGISRuntime.Xamarin.Forms"
x:Class="SGPI.MapPages.MapPage">
<ContentPage.Resources>
<ResourceDictionary>
<local:MapViewModel x:Key="MapViewModel" />
</ResourceDictionary>
</ContentPage.Resources>
<Grid>
<esriUI:MapView Map="{Binding Map, Source={StaticResource MapViewModel}}" x:Name="map" GeoViewTapped="Map_GeoViewTapped"/>
</Grid>
<ContentPage.ToolbarItems>
<ToolbarItem Icon="dan.png" Clicked="Done"/>
</ContentPage.ToolbarItems>
</ContentPage>
Second Page Code Behind
using Esri.ArcGISRuntime.Data;
using Esri.ArcGISRuntime.Geometry;
using Esri.ArcGISRuntime.Symbology;
using System.Collections.Generic;
using Xamarin.Forms;
using Esri.ArcGISRuntime.UI;
using System;
using System.Threading;
namespace SGPI.MapPages
{
public partial class MapPage : ContentPage
{
public event EventHandler<GraphicsOverlay> AcceptedMap;
List<MapPoint> points;
static SimpleLineSymbol symbol = new SimpleLineSymbol()
{
Style = SimpleLineSymbolStyle.Dash,
Color = System.Drawing.Color.Black,
Width = 1
};
static SimpleMarkerSymbol marker = new SimpleMarkerSymbol()
{
Color = System.Drawing.Color.Pink,
Outline = symbol,
Style = SimpleMarkerSymbolStyle.Diamond,
Size = 10
};
static SimpleLineSymbol line = new SimpleLineSymbol(SimpleLineSymbolStyle.Solid, System.Drawing.Color.CadetBlue, 1);
static SimpleFillSymbol fill = new SimpleFillSymbol(SimpleFillSymbolStyle.Solid, System.Drawing.Color.Black, line);
public MapPage(GraphicsOverlay graphic)
{
InitializeComponent();
map.Map = new SGPI.Shared.MapViewModel().Map;
if(graphic == null)
graphic = new GraphicsOverlay();
map.GraphicsOverlays.Add(graphic);
points = new List<MapPoint>();
}
private Graphic AddPolygonInMap(MapPoint[] points)
{
var pointCollection = new PointCollection(points[0].SpatialReference);
foreach (MapPoint p in points)
pointCollection.Add(p);
var sPolygon = new Polygon(pointCollection);
return new Graphic(sPolygon, fill);
}
private void AddPointinMap(MapPoint point)
{
Graphic graphic = new Graphic(point, marker);
map.GraphicsOverlays[0].Graphics.Add(graphic);
}
private async void Map_GeoViewTapped(object sender, Esri.ArcGISRuntime.Xamarin.Forms.GeoViewInputEventArgs e)
{
var tolerance = 10d; // Use larger tolerance for touch
var maximumResults = 1; // Only return one graphic
var onlyReturnPopups = false; // Don't return only popups
// Use the following method to identify graphics in a specific graphics overlay
IdentifyGraphicsOverlayResult identifyResults = await map.IdentifyGraphicsOverlayAsync(
map.GraphicsOverlays[0],
e.Position,
tolerance,
onlyReturnPopups,
maximumResults);
// Check if we got results
if (identifyResults.Graphics.Count > 0)
{
// Make sure that the UI changes are done in the UI thread
Device.BeginInvokeOnMainThread(async () => {
await DisplayAlert("", "Tapped on graphic", "OK");
});
} else
{
points.Add(e.Location);
AddPointinMap(e.Location);
}
}
private async void Done(object sender, EventArgs e)
{
if(points.Count > 2) {
GraphicsOverlay graphics = new GraphicsOverlay();
graphics.Graphics.Add(AddPolygonInMap(points.ToArray()));
map.GraphicsOverlays.Add(graphics);
Thread.Sleep(500);
var accepted = await DisplayAlert("Aviso", "Este é o polígono certo?", "Sim", "Não");
if (accepted)
{
GraphicsOverlay graph = graphics;
map.GraphicsOverlays.Clear();
AcceptedMap?.Invoke(this, graph);
}
else
{
for(int i = 1; i < map.GraphicsOverlays.Count; i++)
map.GraphicsOverlays[i] = new GraphicsOverlay();
}
}
}
}
}
I am using a BoxView to accomplish underlining in my app. I have a couple of labels that are very short - Text such as Yes or No etc. Here is the XAML for one of the labels with the BoxView for underlining:
<StackLayout Orientation="Vertical" Grid.Row="5" Grid.Column="1" Margin="0,4,0,4" HorizontalOptions="Start" BackgroundColor="Purple" MinimumWidthRequest="1">
<Label x:Name="txtUseMetric" TextColor="Blue" FontSize="Small" Text="{Binding UseMetricText}" BackgroundColor="Yellow">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="Value_Tapped" CommandParameter="usemetric" />
</Label.GestureRecognizers>
</Label>
<BoxView BackgroundColor="Green" HeightRequest="1" MinimumWidthRequest="1" />
</StackLayout>
My problem is that the width of the BoxView is always extending past my text I have tried overriding the MinWidthRequest in my App.Xaml file as seen below:
<Style TargetType="BoxView">
<Setter Property="MinimumWidthRequest" Value="3" />
</Style>
But this has not effect. I have included screen shots for you to see.
FYI - The yellow is the width of the Label. You don't see any purple (the background color of the StackLayout) because the StackLayout and Label are the same width. The second screen shot shows what the screen looks like if I remove the BoxView - i.e. the Label and StackLayout are sized correctly.
Any suggestions on how to fix this?
Screen shot with BoxView Too Long making label and StackLayout too long
Screen shot with BoxView removed and Label and Stack Layout sizing correctly
Please note the default HorizontalOptions and that Label derives from View:
Default value is LayoutOptions.Fill unless otherwise documented.
Add HorizontalOptions="Start" on the "Use Metric" Label:
<Label x:Name="txtUseMetric" TextColor="Blue" FontSize="Small"
Text="{Binding UseMetricText}" BackgroundColor="Yellow"
HorizontalOptions="Start">
<BoxView BackgroundColor="Green" HeightRequest="1"
WidthRequest="{Binding Path=Width, Source={x:Reference txtUseMetric}"
HorizontalOptions="Start"/>
One option is to replace the label/box underline with a custom renderer that adds an underline capability to the label.
Here is how to do it:
User Control
public class CustomLabel : Label
{
public static readonly BindableProperty IsUnderlinedProperty =
BindableProperty.Create(nameof(IsUnderlined), typeof(bool), typeof(CustomLabel), false);
public bool IsUnderlined
{
get { return (bool)GetValue(IsUnderlinedProperty); }
set
{
SetValue(IsUnderlinedProperty, value);
}
}
}
Android renderer
[assembly: ExportRenderer(typeof(CustomLabel), typeof(CustomLabelRenderer))]
namespace Incident.Droid.CustomRenderers
{
public class CustomLabelRenderer : LabelRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
var view = (CustomLabel)Element;
var control = Control;
UpdateUi(view, control);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
var view = (CustomLabel)Element;
if (e.PropertyName == CustomLabel.IsUnderlinedProperty.PropertyName)
{
Control.PaintFlags = view.IsUnderlined ? Control.PaintFlags | PaintFlags.UnderlineText : Control.PaintFlags &= ~PaintFlags.UnderlineText;
}
}
static void UpdateUi(CustomLabel view, TextView control)
{
if (view.FontSize > 0)
{
control.TextSize = (float)view.FontSize;
}
if (view.IsUnderlined)
{
control.PaintFlags = control.PaintFlags | PaintFlags.UnderlineText;
}
}
}
}
iOS Renderer
[assembly: ExportRenderer(typeof(CustomLabel), typeof(CustomLabelRenderer))]
namespace Incident.iOS.CustomRenderers
{
public class CustomLabelRenderer : LabelRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
var view = (CustomLabel)Element;
UpdateUi(view, Control);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
var view = (CustomLabel)Element;
if (e.PropertyName == CustomLabel.IsUnderlinedProperty.PropertyName)
{
UpdateUi(view, Control);
}
}
private static void UpdateUi(CustomLabel view, UILabel control)
{
var attrString = new NSMutableAttributedString(control.Text);
if (view != null && view.IsUnderlined)
{
attrString.AddAttribute(UIStringAttributeKey.UnderlineStyle,
NSNumber.FromInt32((int)NSUnderlineStyle.Single),
new NSRange(0, attrString.Length));
}
control.AttributedText = attrString;
}
}
}
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