xamarin mvvm update from different class - c#

I read several similar issues with MVVM, but just cannot find the right fix to this problem. I am sure it's really simple, but I cannot work out how to update an UI from two different classes.
I have one ModelView (Homepage.xaml) and MVVM binded label, which works fine from the Homepage.cs, but how can I update this label from from a different class (anotheractivty.cs). Is there a way to reference the string from from the second class file or do I need to somehow call and pass the string between the anotheractivty to the homepage class?
Many thanks
Please note the code is simplified:
Homepage.xaml
<Label
Text={"Binding Updatetext}"
/>
Homepage.xaml.cs
String updatetext = "";
public Homepage()
{
BindingContent = this;
}
Public string Updatetext
{
get=> updatetext
set
{
if (value == updatetext)
return;
updatetext = value;
OnPropertyChange(nameof(Updatetext));
}
}
Public updatetest()
{
Updatetext = "new text";
}
anotheractivty.cs
How to link this to the Homepage.cs?
Public updatetest()
{
var page = new Homepage;
Homepage.Updatetext = "new text";
}

Related

CollectionView ItemsSource binding visual update (Xamarin.Forms)

I tried to make some MVVM pattern into my app, and i ran into a problem with hte visual representation of data. The data if the binded observablecollecrion is updated, but the visual is not.
some code:
ViewModel:
public class HlavnaViewModel : BaseViewModel
{
public HlavnaViewModel()
{
}
private Doklady _selectedDok;
public Doklady vm_selectedDok
{
get => _selectedDok;
set
{
_selectedDok = value;
OnPropertyChanged(nameof(vm_selectedDok));
update_polozky();
}
}
public async void update_polozky()
{
Polozky dok = new Polozky() { id_doklad = _selectedDok.id };
ObservableCollection<Polozky> pol = new ObservableCollection<Polozky>(await App.Database.GetPolozkyAsync(dok));
vm_polozky = pol;
}
private ObservableCollection<Polozky> _polozky;
public ObservableCollection<Polozky> vm_polozky
{
get => _polozky;
set
{
_polozky =value;
OnPropertyChanged(nameof(vm_polozky));
}
}
}
in the XAML:
<CollectionView x:Name="polozky" SelectionMode="Single" ItemsSource="{Binding vm_polozky}">...
BaseViewModel:
public class BaseViewModel : INotifyPropertyChanged
{
string title = string.Empty;
public string Title
{
get { return title; }
set { SetProperty(ref title, value); }
}
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;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
finally in View:
public Hlavna()
{
InitializeComponent();
hvm = new HlavnaViewModel();
this.BindingContext = hvm;
}
if i select a row in CollectionView where the vm_selectedDok binding is set, it selects that item, fires the update_polozky(), the vm_polozky gets populated with the right data, but the visual just dont shows the items from vm_polozky.
Ive read couple of similar questions, but i cant figure out where i made a mistake.
EDIT:
so the problem was somewhere else, i had the grid.rowdefinitions set just wrong, therefore the grid was outside of the visible area.
#ToolmakerSteve made good suggestions on calling async/await, please read his answer.
There are two alternative ways to fix this.
One way is Gerald's answer. This is fine for small collections, but might be slower if there are many items being added.
The second way is to do what you've done - replace the collection. But there is a fix needed in your code.
The way you've called update_polozky won't work reliably. You don't start the async/await sequence inside an await.
Replace:
update_polozky();
With:
Device.BeginInvokeOnMainThread(async () => {
await update_polozky();
});
OPTIONAL: Might also make this change. (Though it shouldn't be necessary.) This gives a good place to put a breakpoint, to see whether "result" gets the expected contents.
Replace:
ObservableCollection<Polozky> pol = new ObservableCollection<Polozky>(await App.Database.GetPolozkyAsync(dok));
With:
var result = await App.Database.GetPolozkyAsync(dok);
ObservableCollection<Polozky> pol = new ObservableCollection<Polozky>(result);
IMPORTANT NOTES FOR OTHER CODERS:
The second approach ("replace the collection") relies on OnPropertyChanged(nameof(vm_polozky)); in the setter of the ObservableCollection.
You have that, so not an issue for you. I mention this for anyone else who might adapt this code.
For example, I've seen people attempt to set the private value directly, e.g.:
// Don't do this to replace the collection. XAML won't know you changed it!
_myPrivateField = new ObservableCollection<MyItem>(result);
I've also seen people try to have an ObservableCollection without a setter:
// This is okay UNLESS you replace the collection - in which case you need `OnPropertyChanged(nameof(MyCollection))` somewhere:
public ObservableCollection<MyItem> MyCollection { get; set; }
Basically it comes down to, don't do this: ObservableCollection<Polozky> pol = new ObservableCollection<Polozky>(await App.Database.GetPolozkyAsync(dok));
Whenever you create a new ObservableCollection it will lose the databinding to the UI. Clear your ObservableCollection with .Clear() and add new items to it with a for loop. For example:
public async void update_polozky()
{
Polozky dok = new Polozky() { id_doklad = _selectedDok.id };
var results = await App.Database.GetPolozkyAsync(dok);
vm_polozky.Clear();
foreach(var item in results)
vm_polozky.Add(item);
}

How to Refresh or Recall Page on Xamarin Forms

So, I have this app where I can choose a car and see the car info... I'm displaying the cars like this.
I'm using the Rg.Plugins.Popup so when I click the car icon, it opens this popup with "my cars"
So now I'm facing a problem which is, when I choose a car, I want to refresh my current page so the car's info can be shown... I'm handling the car button click on this next view model:
public class MyCarViewModel : ViewModelBase
{
public MyCarViewModel()
{
}
public MyCarViewModel(INavigation navigation)
{
this.Navigation = navigation;
this.SelectedCar = null;
GetClientCars();
}
private Page page { get; set; }
private List<CarInfo> _CarList;
public List<CarInfo> CarList
{
get
{
return _CarList;
}
set
{
_CarList = value;
OnPropertyChanged("CarList");
}
}
private CarInfo _SelectedCar;
public CarInfo SelectedCar
{
get
{
return _SelectedCar;
}
set
{
_SelectedCar = value;
OnPropertyChanged("SelectedCar");
if (_SelectedCar != null)
{
CarSelected(_SelectedCar);
}
}
}
public INavigation Navigation { get; set; }
private void CarSelected(CarInfo car)
{
App.choosedCar = car;
PopupNavigation.Instance.PopAllAsync();
this.SelectedCar = null;
}
}
And I want this View to refresh
<views:BaseMainPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="OficinaDigitalX.Views.CarDetails"
xmlns:views="clr-namespace:OficinaDigitalX.Views">
<views:BaseMainPage.Content>
<StackLayout>
<Label Text="{Binding VID, StringFormat='Modelo: {0:F0}'}" FontAttributes="Bold"
FontSize="Large"/>
<Label Text="{Binding LicencePlate, StringFormat='Matrícula: {0:F0}'}"/>
<Label Text="{Binding Chassis, StringFormat='Chassis: {0:F0}'}"/>
<Label Text="{Binding Km, StringFormat='Ultimos Km Registados: {0:N0}'}"/>
</StackLayout>
</views:BaseMainPage.Content>
</views:BaseMainPage>
and xaml.cs
public partial class CarDetails : BaseMainPage
{
public CarDetails(CarInfo car)
{
InitializeComponent();
BindingContext = new CarDetailViewModel(this);
App.currentPage = this;
if (car != null)
{
this.Title = "Dados de " + car.MakerandModel;
}
else
{
this.Title = "Escolha uma Viatura";
}
}
}
I'm facing a lot of issues here because my car icon is a part of my "BaseMainPage" which is extended by the other Views (so the icon can be shown on all views)...
So when I click the button, the application doesn't know its current page...
I thought I might use the Navigation Stack to reload it but I don't quite know how to do this...
Hope you guys can help
Well, essentially you do not need to refresh page or reload page, you just need to refresh the data.
since you are using OnPropertyChanged(INotifyPropertyChanged) you are half way there.
instead of using List CarList use ObservableCollection CarList.
and if you deliberately want to reload the page, on dismissing the pop.up save your data and call the constructor/reinitiate the Page.
hopefully you should achieve what you are looking for.
I think you don't need to reload the page, you need to reload your data. Your page will be updated automatically with the databindings.
For me it looks like you're using Prism, so you could override the OnNavigatingTo Method and load the data every time the page is "opened".
I've just used MessagingCenter and I've called it with my OnPropertyChanged and this seemed to do the work! Thanks a lot!
View Model:
OnPropertyChanged("SelectedCar");
if (_SelectedCar != null)
{
CarSelected(_SelectedCar);
MessagingCenter.Send(this, "Hi");
}
My other view model's constructor
MessagingCenter.Subscribe<MyCarViewModel>(this, "Hi", (sender) => {
this.currentCar = App.choosedCar;
});

How to use an object in ViewModel coming from code behind

I have a set of pages between which I navigate and between which I need to pass some parameters, namely some objects of the type of my Models. When I navigate to a page I Setup this new page's constructor with a parameter of the type of the object I need, so the class is implemented as follows:
public partial class ArtigoEdit : ContentPage
{
EditionsViewModel viewModel;
public ArtigoEdit(Models.Artigo artigo)
{
InitializeComponent();
BindingContext = viewModel = new EditionsViewModel();
this.viewModel.Artigo = artigo;
}
}
As you can see I'm also using the MVVM pattern and on its side I have a variable "Artigo" to which I want to assign the object my constructor receives:
Models.Artigo artigo;
public Models.Artigo Artigo
{
get { return artigo; }
set { artigo = value; this.Notify("Artigo"); }
}
The problem is that if I try to access the attributes of this object in order to change the way they are displayed and bind to these new variables instead the object is null when the page opens so the variables are not shown:
string tipoArtigo = "";
public string TipoArtigo
{
get { return tipoArtigo; }
set
{
if (Artigo != null)
{
if(Artigo.TipoArtigo == "P")
tipoArtigo = "Produto";
else if (Artigo.TipoArtigo == "S")
tipoArtigo = "Serviço";
else if (Artigo.TipoArtigo == "O")
tipoArtigo = "Outro";
else if (Artigo.TipoArtigo == "I")
tipoArtigo = "Imposto";
else
tipoArtigo = value;
}
this.Notify("TipoArtigo");
}
}
Is this not the correct way of doing this? If not, what is my alternative? Beware that my ViewModels all implemente a ViewModelBase class that itself implements INotifyPropertyChanged so do not worry about that! :)
Silly me, I'm sorry for disturbing and I thank to all the people who tried to help. What was happening was: I was binding these variables to an Entry, but for some reason it was not expanding to fit the size of information so although it was there it did not appear to be...luckily I've noticed it.

Confused as to where to place logic code in ViewModel

I'm new to C#/WPF and I would like some clarification on whether I have the proper implementation of my ViewModel.
I have created a simple window with a search text box and list box for the results.
<TextBox Text="{Binding SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<ListBox ItemsSource="{Binding Results}" />
Then I have a ViewModel with the following code.
private List<string> lstStr;
public ViewModel()
{
lstStr = new List<string>();
lstStr.Add("Mike");
lstStr.Add("Jerry");
lstStr.Add("James");
lstStr.Add("Mikaela");
}
public List<string> LstStr
{
get
{
return lstStr;
}
set
{
if (lstStr != value)
{
lstStr = value;
OnPropertyChanged("LstStr");
}
}
}
private string searchText;
public string SearchText
{
get
{
return searchText;
}
set
{
if (searchText != value)
{
searchText = value;
OnPropertyChanged("SearchText");
UpdateResults();
}
}
}
private ObservableCollection<string> results = new ObservableCollection<string>();
public ObservableCollection<string> Results
{
get
{
return results;
}
set
{
if (results != value)
{
results = value;
OnPropertyChanged("Results");
}
}
}
public void UpdateResults()
{
int i = 0;
results.Clear();
while (i < LstStr.Count)
{
if (LstStr.ElementAt(i).ToString() != null)
{
if (searchText != null && searchText != "")
{
if (LstStr.ElementAt(i).Trim().Contains(searchText))
{
results.Add(LstStr.ElementAt(i));
Console.WriteLine(LstStr.ElementAt(i));
}
}
else
results.Clear();
}
else
Console.WriteLine("NULL");
i++;
}
}
I see myself writing logic in the Get or Set section of code in the ViewModel. Let's say I will have more text boxes and lists that will want to implement. Is this the correct way of coding my logic in the properties or am I completely missing the point? Please help me understand this. Thanks in advance.
No, this isn't exactly right.
First, logic normally goes in the model, not the view model. That said, you have a filter, which is basically UI logic, so its probably OK here.
Second, the filter will only change when you set the search text, so the logic would go in the setter, not the getter. I also wouldn't inline the whole thing, put it in its own function so you can reuse it later:
public String SearchText
{
...
set
{
serachText = value;
NotifyPropertyChanged();
UpdateResults();
}
}
public void UpdateResults()
{
...
}
The one thing to keep in mind (and there isn't really a good way around this) is that if that function takes a long time to run, your UI will really slow down while the user is typing. If the execution time is long, try shortening it, then consider doing it on a separate thread.
ViewModels should have only the responsibility of "converting" data into another form that the view can handle (think INotifyPropertyChanged, ObservableCollection, etc.)
The only time where you'd get away with the ViewModel having any of the logic is when the logic is encapsulated entirely in a collection. e.g. if you can get everything you need out of List<T>, then the ViewModel effectively has all the logic. If you need value beyond that, it should be outside of the ViewModel.

Custom Label does not show the Text string

I needed to make my own label to hold some value, that is diferent from the value displayed to user
public class LabelBean : Label {
private string value;
public LabelBean(string text = "", string value = ""): base() {
base.Text = text;
this.value = value;
}
public string Value {
get { return value; }
set { this.value = value; }
}
}
but now id in the form constructor I replace the control with my class
this.lbAttributeType = new LabelBean();
and later after the form is created, but before it is shown I set the text through setter
(this.lbAttributeType as LabelBean).Value = value;
this.lbAttributeType.Text = Transform(value);
but in the form I have always "label1" text... what is wrong with it?
thanks
UPDATE
I added the solution here to find it easier:
public class MyLabel : Label {
public MyLabel()
: base() {
}
public string Value {
set {
this.Text = value;
}
}
}
and the form with Widnows.Forms.Label label1 control
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
this.Controls.Remove(this.label1);
this.label1 = new MyLabel();
this.Controls.Add(this.label1);
(this.label1 as MyLabel).Value = "oh";
}
}
the bug was in the Controls.Remove and Controls.Add,
thanks all for their time :)
My guess is because, since you're doing the work in the constructor, the InitializeComponent code, automatically generated by the designer, is overwriting the control instance, as it's most likely called after your initialisation.
If the class is part of the project, you will find it on the toolbox; meaning you can simply drag and drop your new control on the form in place of the existing one - this is what you should do.
This ensures that the designer-generated property is of your LabelBean type, and not simply Label.
Also - you should consider changing your Value setter as demonstrated by WoLfulus (+1 there)
Update
In response to the comment you put on WoLfulus' answer - here's a couple of alternatives:
1) If the form is the 'clever' bit here - consider just writing a helper method in it, and setting the value of the label through it, leveraging the Tag property:
public void SetLabelBean(Label target, string value)
{
Label.Tag = value;
Label.Text = Transform(value);
}
public string GetLabelBean(Label target)
{
return target.Tag as string;
}
2) Continue using your sub-classed LabelBean type (adding it via the designer as I've already mentioned) - but use an abstraction to give it access to the form's Transform method:
public interface ITransformProvider
{
string Transform(string);
}
Make your form class implement this interface, with the Transform method you elude to.
Now, in your LabelBean class:
public ITransformProvider Transformer
{
get{
//searches up the control hierarchy to find the first ITransformProvider.
//should be the form, but also allows you to use your own container controls
//to change within the form. The algorithm could be improved by caching the
//result, invalidating it if the control is moved to another container of course.
var parent = Parent;
ITransformProvider provider = parent as ITransformProvider;
while(provider == null){
parent = parent.Parent;
provider = parent as ITransformProvider;
}
return provider;
}
}
And then, finally, using WoLfulus' code, but slightly changed, you can do this:
public string Value
{
get
{
return value;
}
set
{
this.value = value;
var transformer = Transformer;
if(transformer != null) this.Text = transformer.Transform(value);
}
}
That, I think, addresses your issues with that answer.
Try this:
Create a new delegate outside the label class:
public delegate string LabelFormatDelegate( string val );
Add this to your label class:
public LabelFormatDelegate ValueFormatter = null;
public string Value
{
get
{
return value;
}
set
{
this.value = value;
if (this.ValueFormatter != null)
{
this.Text = this.ValueFormatter(value); // change the label here
}
else
{
this.Text = value;
}
}
}
Place a new common label to your form (lets name it "label1")
Goto to Form1.Designer.cs and search for "label1" declaration.
Rename the "Label" type to your own label type (Ex: "MyLabel")
Change the initialization code of label on InitializeComponent function on designer code to match the new type "MyLabel"
Example:
this.label1 = new Label();
Change to:
this.label1 = new MyLabel();
In the Form_Load event, specify the format function:
this.label1.ValueFormatter = new LabelFormatDelegate(this.Transform);
Notes: You'll need to remove the "Text" setter call too from here:
(this.lbAttributeType as LabelBean).Value = value;
// this.lbAttributeType.Text = Transform(value);
This will keep your value/text in sync but remember not to set "Text" property by hand.
I agree with WoLfulus and Andreas Zoltan and would add a symmetrical functionality to Text if there exists a unambiguous reverse transformation:
public string Value
{
get { return value; }
set
{
if (this.value != value) {
this.value = value;
this.Text = Transform(value);
}
}
}
public override string Text
{
get { return base.Text; }
set
{
if (base.Text != value) {
base.Text = value;
this.value = TransformBack(value);
}
}
}
Note the if checks in order to avoid an endless recursion.
EDIT:
Assigning your label to lbAttributeType is not enough. You must remove the old label from the Controls collection before the assignment and re-add it after the assignment.
this.Controls.Remove(lbAttributeType); // Remove old label
this.lbAttributeType = new LabelBean();
this.Controls.Add(lbAttributeType); // Add new label
Your form was still displaying the old label! Why did I not see it earlier?

Categories