Question
Two-Way DataBinding allows you to automatically populate UI components with data from an Object and then automatically update the Object as the user edits those UI components.
As the user edits UI components, is there a way to not only automatically update the Object in memory, but automatically update/persist the Object in a Room database?
I could manually listen to every UI field modification and manually save the Object to the Room Database. However, such a manual, brute force approach would negate the benefits of Two-Way DataBindings that I'm hoping to utilize.
Context
My application stores Items in an SQLite database using Android's Room Persistence Library. This is a simplified version of my Item:
#Entity
public class Item {
#PrimaryKey(autoGenerate = true)
private long id;
private String name;
public long getId() {
return this.id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
The magic of ViewModel, LiveData, and Two-Way DataBindings allows my ItemEditorFragment to automatically populate the UI with data from the selected Item and to update that Item when the user edits the UI components:
#Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof ItemViewModelProvider) {
final ItemViewModelProvider itemViewModelProvider = (ItemViewModelProvider) context;
mViewModel = itemViewModelProvider.getItemViewModel();
} else {
throw new RuntimeException(context.toString()
+ " must implement ItemViewModelProvider");
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final FragmentItemEditorBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_item_editor, container, false);
binding.setViewModel(mViewModel);
binding.setLifecycleOwner(this);
final View view = binding.getRoot();
return view;
}
This is a simplified version of the layout being inflated:
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.lafave.ItemViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#={viewModel.selectedItem.name}"
android:layout_gravity="center_horizontal"/>
</LinearLayout>
</layout>
One of the dirty hacks I've made work is: creating a new property, in the ViewModel (but it doesn't strictly have to be exactly there), without a backing field (in Java, this would probably equate to creating a getter and a setter, without a field):
var selectedItemName: String?
get() = selectedItem.value?.name
set(value) {
selectedItem.value?.let {
it.name = value
dao.update(it)
}
}
This is not an ideal solution, since it requires mirroring all of the properties in the class, but it works if you just want to get it over with. I'm awaiting a better solution!
Related
I am making a form With Cascaded dropdowns I got the logic working using the select component of blazor but when I try to use it on mudselect the #onchange function doesn't fire I have no idea how to use the Valuechanged EventCallback of the same so I am stuck on what to do. if anyone has done cascading in Mudblazor I am intrigued by how it was done
I am not 100% sure this answers your problem but due to a lack of details this is the first step. As you found out MudSelect offers the ValueChanged callback that you can hook into. You write your own method that the component shall call when the value changes. In the sample below that's void OnBeverageChanged(). Now you reference it as parameter of MudSelect.
You can run the code below with Try MudBlazor and experiment with it until you have what you need.
<MudSelect T="string" Label="Coffee" AnchorOrigin="Origin.BottomCenter" ValueChanged=OnBeverageChanged>
<MudSelectItem Value="#("Cappuccino")" />
<MudSelectItem Value="#("Cafe Latte")" />
<MudSelectItem Value="#("Espresso")" />
</MudSelect>
<MudText>
How many times did you change your beverage ... do you know it's: #TimesChanged
</MudText>
#code {
public class Pizza
{
public Pizza(string name)
{
Name = name;
}
public readonly string Name;
// Note: this is important so the MudSelect can compare pizzas
public override bool Equals(object o) {
var other = o as Pizza;
return other?.Name==Name;
}
// Note: this is important too!
public override int GetHashCode() => Name?.GetHashCode() ?? 0;
// Implement this for the Pizza to display correctly in MudSelect
public override string ToString() => Name;
}
private int TimesChanged = 0;
private void OnBeverageChanged()
{
TimesChanged++;
StateHasChanged();
}
}
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
I've been using MvxAppCompatActivity throughout my project, but in this specific case I have to make use of a MvxAppCompatDialogFragment.
Unfortunately, in this case I lose the binding context of the ViewModel somehow.
MobileTestView
[MvxDialogFragmentPresentation]
[Register(nameof(MobileScreenTestView))]
public class MobileTestView : MvxAppCompatDialogFragment<MobileTestViewModel>
...
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
return inflater.Inflate(Resource.Layout.mobile_screen, container, false);
}
...
MobileTestViewModel
public class MobileTestViewModel : MvxViewModel<MInput, MResult>
...
public string Instructions { get; set; } = "Instructions";
...
mobile_screen.axml
...
<TextView
android:id="#+id/text_mobile"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:gravity="center_vertical|center_horizontal"
tools:text="Scan"
local:MvxBind="Text Instructions" />
...
local:MvxBind="Text Instructions" does not work anymore, but I've checked and it is set in the view model before it gets to OnCreateView().
The above code would work fine for a MvxAppCompatActivity.
If what I'm trying to do is not possible, I can always do it like
view.FindViewById<TextView>(Resource.Id.text_mobile).Text = ViewModel.Instructions;
It's not that I really need to use local:MvxBind, but I would like to know what I'm doing wrong.
Update - For anyone having the same problem:
Change the OnCreateView method to:
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
base.OnCreateView(inflater, container, savedInstanceState);
return this.BindingInflate(Resource.Layout.mobile_screen, container, false);
}
and your BindingContext will work fine.
As you noticed yourself, you had to use this.BindingInflate instead of the LayoutInflater argument in OnCreateView. This is because we have no way to intercept the Fragment lifecycle in MvvmCross to provide our own Layout Inflater.
What BindingInflate does, is to run through the view hierarchy and look for all the custom attributes applied on views, in your case Text Instructions and apply these bindings between the View and the ViewModel.
So whenever working with Fragments, you should use BindingInflate.
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.
I am currently trying to dynamically add a new component to the JSF component tree during an ajax request.
In fact I add a child to the UIViewRoot component in my AjaxBehaviorListener which is fired on server side during the ajax request process.
The issue is that the new component is not rendered. It seems that this component is not taken into account in the render response phase.
Could you help me on this issue ?
Regards,
Guillaume
This solution works in case of you know before the ajax request the component to add.
But if you are not able to know which component to add, it does not work.
I maybe found a solution :
My solution is to implement my custom PartialViewContext and use the method startInsertAfter or startInsertBefore of the PartialResponseWriter.
It is working, but you have to put the component added as transient. (uiComponent.setTransient(Boolean.TRUE);)
Regards,
Guillaume
This works for me:
Bean holding binding to UIComponent under which you want to add other UIComponents dynamically should be request scoped otherwise it can throw some nasty exceptions (don't ask me why):
#ManagedBean
#RequestScoped
public class AddressEditorBean {
// session bean reference for holding id number line and model
#ManagedProperty(value = "#{addressValueBean}")
private AddressValueBean address;
public String addOutputText() {
HtmlOutputText text = new HtmlOutputText();
int c = address.getC();
text.setValue("new text! " + c);
text.setId("id" + c++);
address.setC(c); // hold id number line in sessionbean
address.getComps().add(text); // hold new uicomponent in session bean to be able to rebuild it
panel.getChildren().clear(); // need to clear children and add them all again,
panel.getChildren().addAll(address.getComps()); // otherwise there are problems with duplicate children (bug?)
return "success";
}
public HtmlPanelGroup getPanel() {
return panel;
}
public void setPanel(HtmlPanelGroup pane) {
if (panel == null) {
this.panel = pane;
if (panel != null) {
panel.getChildren().addAll(address.getComps());
}
} else {
this.panel = pane;
}
}
}
code snippet from page. I dynnamically add components to <h:panelGroup>
<h:form>
<h:panelGroup id="section" binding="#{addressEditorBean.panel}">
</h:panelGroup>
<h:commandButton value="add new text" action="#{addressEditorBean.addOutputText}">
<f:ajax execute="#this" render="section" event="action"/>
</h:commandButton>
</h:form>
In Session bean I hold actual dynamic model so that I can rebuild it after page reload:
#ManagedBean
#SessionScoped
public class AddressValueBean extends ValueBean<Address> {
private int c = 0;
private List<UIComponent> comps = new ArrayList<UIComponent>();
public AddressValueBean() {
setValue(new Address());
}
public int getC() {
return c;
}
public void setC(int cn) {
this.c = cn;
}
public List<UIComponent> getComps() {
return comps;
}
public void setComps(List<UIComponent> comps) {
this.comps = comps;
}
}
Instead of trying to add the component dynamically during the ajax request, try defining the component upfront, and setting it's rendered tag to false. The contents of the component can then be populated with the ajax request, and the rendered attribute flipped to display the attribute.