Xamarin Forms - force a control to refresh value from binding - xamarin

Given the following ViewModel...
public class NameEntryViewModel
{
public NameEntryViewModel()
{
Branding = new Dictionary<string, string>();
Branding.Add("HeaderLabelText", "Welcome to the app");
}
public Dictionary<string, string> Branding { get; set; }
}
Bound to the 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"
x:Class="Monaco.Forms.Views.NameEntryPage">
<Label Text="{Binding Branding[HeaderLabelText]}" />
</ContentPage>
When the page comes up the Label will get the text 'Welcome to the app'. This works great and fits into our plan for being able to customize and globalize our app. Then Branding dictionary is statically set in this example, but in the real world it is initialized by data from a service call.
However, should the user want to switch the language to Spanish, we would need each bound label to update to the new value. It's easy enough to reset the Branding dictionary and fill it with Spanish translations, but how can we force the controls to refresh from their bound sources?
I am trying to avoid two-way data binding here b/c we don't want the code overhead of creating a backing property for each Text property of the controls. Hence we are binding to a dictionary of values.
ANSWER
I accepted the answer below, but I didn't use a traditional property setter. Instead when a user wants to toggle a different language, we now have a centralized handler that repopulates our Dictionary and then notifies of the change to the Dictionary. We are using MVVMCross, but you can translate to standard forms...
public MvxCommand CultureCommand
{
get
{
return new MvxCommand(async () =>
{
_brandingService.ToggleCurrentCulture();
await ApplyBranding(); // <-- this call repopulates the Branding property
RaisePropertyChanged(() => Branding);
});
}
}

As #BillReiss mentioned you need to use the OnPropertyChanged event inherit the NameEntryViewModel from this class:
public class BaseViewModel : INotifyPropertyChanged
{
protected BaseViewModel ()
{
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged ([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler (this, new PropertyChangedEventArgs (propertyName));
}
}
And create a private property that you can assign to you public property something like:
Dictionary<string, string> _branding = new Dictionary<string, string>();
public Dictionary<string, string> Branding
{
get
{
return _branding;
}
set
{
_branding = value;
OnPropertyChanged(nameof(Branding));
}
}
And with this every time you set the Branding property it will let the View know that something changed! Sometimes if you are doing this on a back thread you have to use Device.BeginInvokeOnMainThread()

In your BaseViewModel class is a method:
protected bool SetProperty<T>(ref T backingStore, T value,
[CallerMemberName] string propertyName = "",
Action onChanged = null) {}
like:
string title = string.Empty;
public string Title
{
get { return title; }
set { SetProperty(ref title, value); }
}
Use SetProperty in the setter-properties of your inherited view-model class.
Then it will work.
INotifyPropertyChanged is already implemeted in BaseViewModel. So nothing to change here.

Related

Dynamic resx translation in Xamarin Forms using MVVM

We have an app written with Pages and no pattern and I want to re-write it using MVVM. Currently we use a Picker for language selection and when the culture changes we set all label.Text controls again in order to redraw them in the new language.
I re-wrote the same page using MVVM and now SelectedItem in the Picker is bound to a Language object. In the setter for SelectedItem I also change the culture of my resx (AppResources.Culture) but the UI bound to it (e.g. Text="{x:Static resources:AppResources.Title) doesn't change language.
Full code in my SelectedItem setter:
SetProperty(ref selectedLanguage, value);
AppResources.Culture = value.Culture;
cultureManager.SetLocale(value.Culture);
How should I update all the Text of my UI? Is there any clean way to do something like this, it seems like a basic translation need... or it wasn't meant to be done, especially not without closing the view/app?
The approaches I found for localization using IMarkupExtension and this thread on Xamarin forums which in the end effectively re-creates the page...
My goal is to ideally reload text without having to re-create the view/close the app, using MVVM and clean code. I have about 10 views so it has to be something reusable.
Create you RESX Resources first. I use en, nl, fr for example.
Create the view model to binding the LocalizedResources.
public class ViewModelBase : INotifyPropertyChanged
{
public LocalizedResources Resources
{
get;
private set;
}
public ViewModelBase()
{
Resources = new LocalizedResources(typeof(LocalizationDemoResources), App.CurrentLanguage);
}
public void OnPropertyChanged([CallerMemberName]string property = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
public event PropertyChangedEventHandler PropertyChanged;
}
In SettingsPage, use a picker to choose the language.
<StackLayout>
<Label Text="{Binding Resources[PickLng]}" />
<Picker ItemsSource="{Binding Languages}" SelectedItem="{Binding SelectedLanguage, Mode=TwoWay}" />
</StackLayout>
View model of SettingsPage.
public class SettingsViewModel : ViewModelBase
{
public List<string> Languages { get; set; } = new List<string>()
{
"EN",
"NL",
"FR"
};
private string _SelectedLanguage;
public string SelectedLanguage
{
get { return _SelectedLanguage; }
set
{
_SelectedLanguage = value;
SetLanguage();
}
}
public SettingsViewModel()
{
_SelectedLanguage = App.CurrentLanguage;
}
private void SetLanguage()
{
App.CurrentLanguage = SelectedLanguage;
MessagingCenter.Send<object, CultureChangedMessage>(this,
string.Empty, new CultureChangedMessage(SelectedLanguage));
}
}
Do not forget to binding the context.
I have upload on GitHub, you could download from DynamicallyBindingRESXResources folder on my GitHub for reference.
https://github.com/WendyZang/Test.git

Binding string with Button

I am trying to bind a string to a Button in pure C# (no XAML), but apparently I am doing it wrong, as the result of my code is that the button disappears.
I am defining my property as follows:
public string selectionString { get; set; }
And this is how I am binding the string to the button:
selectionString = "Hello";
selectionButton = new Button
{
TextColor = Color.Black
};
selectionButton.SetBinding(Button.TextProperty, "selectionString");
Children.Add(selectionButton);
I have tried to use BindingMode.TwoWay, but it doesn't work.
Of course, setting the text and removing the binding makes the button appear and work.
My need is just this: the button text should be the selectionString, and if this changes by an external event, so the button's text should change accordingly.
Am I missing something in how the binding works?
Bindings work against public properties on the view's binding context, and respond to INotifyPropertyChanged events firing. Hopefully this demonstrates for you.
public class MyViewModel : INotifyPropertyChanged
{
// Fire RaisePropertyChanged in the setter, I use Fody to weave this in
public string SelectionString {get;set;}
}
public class MyView : Page
{
protected override void OnBindingContextChanged()
{
if (BindingContext is MyViewModel)
{
this.SetBinding(Button.TextProperty, "SelectionString");
}
}
}

Xamarin binding EventHandler

How can I bind event handler from xaml to ViewModel
in my xamal:
<Entry Text="{Binding SecondName , Mode=TwoWay}" Focused="{Binding FocusedEventhandler}" Completed="{Binding Compleated}">
How can i get that event handler actions in my viewModel?
Try this way
public class TestViewModel
{
public ICommand FocusedEventhandler { get; private set; }
public TestViewModel()
{
FocusedEventhandler = new Command(async () => await ShowContactList());
}
async Task ShowContactList()
{
//your code here
}
}
Call your ViewModel from your page
public partial class TestPage: ContentPage {
public AddContact() {
InitializeComponent();
BindingContext = new TestViewModel();
}
This is just overview, to give you an idea how we can do it.
I believe the proper answer to this question is now to use Behaviors to map from Event Handler to Behavior to Command, in the case that the UI Control's XAML property, e.g. SearchBar.TextChanged, expects an event handler. Please see the documentation here: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/behaviors/creating
and here: https://devblogs.microsoft.com/xamarin/turn-events-into-commands-behaviors/

Why is an item in the ViewBag not propagated to the _Layout page?

I have a SiteNavigation class that is being updated in the Initialize event of my base controller e.g.
[Serializable]
public class SiteNavigation
{
public SiteNavigation()
{
IsSummarySelected = true;
}
public Model.Dtos.Folder[] Folders { get; set; }
public bool IsSummarySelected { get; set; }
}
protected override void Initialize(System.Web.Routing.RequestContext requestContext)
{
base.Initialize(requestContext);
var siteNavigation = new SiteNavigation();
siteNavigation.Folders = GetMeMyFolders() as Folder[];
ViewBag.SiteNavigation = siteNavigation;
}
and in the controller the IsSummarySelected property is changed to this value.
ViewBag.SiteNavigation.IsSummarySelected = false;
When I access the property in the _Layout file with this line of code, the value is ALWAYS true. It's as if the nav object is being New'd up again and the constructor is setting it to true.
#if (ViewBag.SiteNavigation.IsSummarySelected)
I've tried casting the nav object back to a variable and setting the property that way too, no dice. Any help would be appreciated.
Call me baffled!
Thank you,
Stephen
I just copy pasted your code into my sample mvc project, and changing IsSummarySelected in my action correctly was reflected in the _Layout file. Are you certain your controller's assignment is getting hit, and you're not reassigning it afterwards somewhere else?
Edit: Your issues are an example of why I think it's a bad idea to use ViewBag for anything other than a localized quick fix. Debugging dynamic global objects is no fun. Refactoring suggestion: Make a site Navigation property in your base controller
SiteNavigation siteNavigation;
public SiteNavigation SiteNavigation
{
get
{
return siteNavigation;
}
set
{
siteNavigation = value;
}
}
and replace all references to ViewBag.SiteNavigation with this. Then create a custom WebViewPage and put in it.
public SiteNavigation SiteNavigation
{
get
{
return ((BaseController)ViewContext.Controller).SiteNavigation;
}
}
This won't fix your problem, but now you can just stick breakpoints on the get and set properties of SiteNavigation, and it should be very easy to debug your issue now.
I fill my TempData["SplitterIsCollapsed"] when Filters are invoked then via OnResultExecuting method. Additionally i fetch a property state from my UserContext class, which is registered only once per session: builder.RegisterType().As().CacheInSession(); .
Basic info: I use DependcyInjection!
Assignment of the Filter to the Controller:
Controller:
[LayoutTempData]
public class HomeController : Controller
{
//....
}
FilterAttribute class:
namespace MyProject.Web.Infrastructure.Filters
{
public class LayoutTempDataAttribute : ActionFilterAttribute
{
private readonly IUserContext _userContext;
public LayoutTempDataAttribute()
{
_userContext = DependencyResolver.Current.GetService<IUserContext>();
}
public override void OnResultExecuting(ResultExecutingContext context)
{
if (context.Controller.TempData.ContainsKey("SplitterIsCollapsed"))
context.Controller.TempData["SplitterIsCollapsed"] = _userContext.LayoutInformation.SplitterIsCollapsed;
else
context.Controller.TempData.Add("SplitterIsCollapsed", _userContext.LayoutInformation.SplitterIsCollapsed);
}
}
}
The Splitter part of the _Layout.cshtml looks like:
#{Html.Telerik().Splitter().Name("Splitter1")
.Panes(panes =>
{
panes.Add()
.Size("300px")
.Collapsible(true)
.Collapsed((bool)TempData["SplitterIsCollapsed"])
.Content(<div>asdfasdf</div>);
panes.Add()
.Collapsible(false)
.Scrollable(false)
.Content(<div>content2</div>);
})
.Render();
}

ItemsControl Doesn't Resort or Refilter After Page First Loaded

I'm creating an application with a weather map that shows the heat as a Pushpin at different places. To do this, supplied by own PushpinModel that supports the INotifyPropertyChanged interface:
public class PushpinModel: INotifyPropertyChanged
{
#region // events
public event PropertyChangedEventHandler PropertyChanged;
#endregion events
#region // fields
Heat heat = Heat.normal;
#endregion fields
#region // properties
public string Placename { get; set; }
public GeoCoordinate Location { get; set; }
public Heat Heat
{
get { return heat; }
set
{
if (heat != value)
{
heat = value;
OnPropertyChanged("Heat");
}
}
}
public string IDno { get; set; }
#endregion properties
#region // handlers
protected virtual void OnPropertyChanged(string propChanged)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propChanged));
}
#endregion handlers
}
The PushpinModel objects are contained in an ObservableCollection called Pushpins which are periodically updated to ShowWeather:
public class Pushpins: ObservableCollection<PushpinModel>
{
#region // METHODS
public void ShowWeather( WeatherReport fromWeatherReport)
{
foreach (WeatherRecord w in fromWeatherReport.WeatherRecords)
{
this.First<PushpinModel>(p => p.IDno == w.PlaceID).Heat = w.Heat;
}
}
#endregion methods
}
I display the Pushpins on a Bing Map, but also as items in an ItemsControl:
<ItemsControl x:Name="ItemList" ItemsSource="{Binding Source={StaticResource placesSortedAndFiltered}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border>
<TextBlock Text="{Binding Placename}" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
The ItemsSource is defined as a CollectionViewSource:
<CollectionViewSource x:Key="placesSortedAndFiltered" Source="{Binding ElementName=MyMainPage, Path=Pushpins}" Filter="PlaceHeat_Filter">
<CollectionViewSource.SortDescriptions>
<componentmodel:SortDescription PropertyName="Placename" Direction="Ascending" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
with a Filter in the codebehind defined as:
private void PlaceHeat_Filter(object sender, FilterEventArgs e)
{
e.Accepted = (((PushpinModel)e.Item).Heat != Heat.na);
}
where a public enum Heat {na,cool,normal,warm,hot}
The problem is that ItemsControl list display appropriately sorted and filtered on page load, but does NOT update when the PushpinModel objects properties are changed. Please note that when the Pushpins object is bound to a Bing Map Control, the PushpinModel objects do update as expected. So somehow, my ItemsControl list isn't updating even though it is bound via a CollectionView to an ObservableCollection
I believe that the CollectionViewSource implementation only supports automatic filtering when the underlying collection is changed (or reset). The filter does not get invoked if a property of an underlying data item changes.
You could either call Refresh() on the CollectionViewSource when a property for an item in the collection changes, or you could implement your own CollectionViewSource which listens to property changed events on underlying data objects, or you could bind directly to a filtered (and sorted) collection rather than using the CollectionViewSource.

Resources