Xamarin.Forms transfer data between screens with MVVM - c#

I have two screens.
The 1st one have two Entries and Save button, the 2nd one - two Labels.
Both have corresponding binded ViewModels.
e.g. 1st XAML:
<Entry x:Name="Entry1" Text="{Binding Entry1}"/>
<Button Command="{Binding SaveCommand}" Text="Save"/>
1st ViewModel:
class Screen1ViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private string entry1;
public string Entry1;
{
get { return entry1; }
set
{
entry1= value;
PropertyChanged(this, new PropertyChangedEventArgs("Entry1"));
}
}
//similar code for Entry2
public ICommand SaveCommand { protected set; get; }
public Screen1ViewModel()
{
SaveCommand = new Command(OnSubmit);
}
public void OnSubmit()
{
//I guess here I supposed to transfer data from 1st screen to 2nd
}
}
Is there any easy way to get strings from 1st screen entries and pass them to 2nd screen labels using ViewModels?

If you don't want these View Models to be coupled or have relationships with each other, you would want some sort of event aggregator or messaging (pub-sub) mechanism. Xamarin Forms comes with a Messaging service out of the box called the Messaging Center to accomplish this.

I've implemented very simplified sample for you.
Of course it's not the best implementation to do the following:
((testApp.App)App.Current).MainPage.Navigation
The best way to implement navigation is to have navigation service like in the following article:
https://mallibone.com/post/a-simple-navigation-service-for-xamarinforms
It's better since in this case your viewModel does not know anything about pages, it knows only string page key. It's also easier to understand code and debug it, since there is a central calling point.
There is also MVVM light toolkit available. The following article demonstrates how to leverage its features and to implement navigation:
https://mobileprogrammerblog.wordpress.com/2017/01/21/xamarin-forms-with-mvvm-light/
Messaging service is the worst thing I can recommend regarding navigation since it's hard to understand code and debugging is a real mess. By decoupling code you make dependent things independent and new people can't get a head or tail of it how the code works. Messaging is good when you pass events from inner viewModels to the root page view model or from view model to view or to page, but it does not suit for navigation task.
My simple sample can be found below:
App code:
public App()
{
InitializeComponent();
MainPage = new NavigationPage(new Views.Page1());
}
Page1.xaml:
<?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="testApp.Views.Page1"
xmlns:local="clr-namespace:testApp.Views;assemply=testApp">
<ContentPage.BindingContext>
<local:Page1ViewModel/>
</ContentPage.BindingContext>
<ContentPage.Content>
<StackLayout>
<Entry Text="{Binding TextPropertyValue}" />
<Button Command="{Binding SaveCommand}" Text="Save"/>
</StackLayout>
</ContentPage.Content>
</ContentPage>
Page1ViewModel:
using System;
using System.ComponentModel;
using System.Windows.Input;
namespace testApp.Views
{
public class Page1ViewModel:INotifyPropertyChanged
{
public Page1ViewModel()
{
SaveCommand = new Xamarin.Forms.Command(HandleAction);
}
async void HandleAction(object obj)
{
await ((testApp.App)App.Current).MainPage.Navigation.PushAsync(
new Page2()
{
BindingContext = new Page2ViewModel(TextPropertyValue)
});
}
string entry1;
public string TextPropertyValue
{
get
{
return entry1;
}
set
{
if (value!=entry1)
{
entry1 = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TextPropertyValue)));
}
}
}
public ICommand SaveCommand
{
get;
set;
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
Page2.xaml:
<?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="testApp.Views.Page2">
<ContentPage.Content>
<StackLayout>
<Label Text="{Binding EntryValue}"/>
</StackLayout>
</ContentPage.Content>
Page2.xaml.cs
public partial class Page2 : ContentPage
{
public Page2()
{
InitializeComponent();
}
}
Page2ViewModel
using System;
namespace testApp.Views
{
public class Page2ViewModel
{
public Page2ViewModel(string entry)
{
EntryValue = entry;
}
public string EntryValue
{
get;
set;
}
}
}

Related

.NET MAUI Binding Issue

Newbie to .NET MAUI and to MVVM. I've seen other examples out there for this, but mine won't work. When I run the code it shows the string PlayProperty in PlayMCanvas as null. I don't know how to get data into the canvas.
VM
public class ShowViewModel
{
public string TheString {get;set; }
public ShowViewModel()
{
TheString = "test";
}
}
View Code Behind
public partial class ShowPlay : ContentPage
{
public ShowViewModel TheVM;
public ShowPlay()
{
InitializeComponent();
TheVM = new ShowViewModel();
TheVM.TheString = "test2";
BindingContext = TheVM;
}
}
XAML View
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:drawables="clr-namespace:PlayMApp.Drawables"
xmlns:local="clr-namespace:PlayMApp"
x:Class="PlayMApp.ShowPlay"
x:DataType="local:ShowViewModel"
Title="Show Play">
<VerticalStackLayout>
<Label
Text="Welcome to .NET MAUI!"
VerticalOptions="Center"
HorizontalOptions="Center" />
<Button
Text="Add Motion"
Clicked="AddMotion"
VerticalOptions="Start"
HorizontalOptions="Center"></Button>
<GraphicsView HeightRequest="300"
WidthRequest="400">
<GraphicsView.Drawable>
<drawables:PlayMCanvas Play="{ Binding TheString }" />
</GraphicsView.Drawable>
</GraphicsView>
</VerticalStackLayout>
</ContentPage>
PlayMCanvas
public class PlayMCanvas : GraphicsView, IDrawable
{
public PlayMCanvas()
{
}
public string Play
{
get => (string)GetValue(PlayProperty);
set => SetValue(PlayProperty, value);
}
public static BindableProperty PlayProperty = BindableProperty.Create(nameof(Play), typeof(string), typeof(PlayMCanvas));
public void Draw(ICanvas canvas, RectF dirtyRect)
{
canvas.StrokeColor = Colors.Red;
canvas.StrokeSize = 6;
canvas.DrawLine(10, 10, 90, 100);
canvas.DrawString(Play,40,30,HorizontalAlignment.Left);
}
}
When I get to the final line (DrawString), I think Play should be "test2", but it's null
I've tried making changes to the code to tweak what is sent in. If I send just a plain literal string through the <drawables:PlayMCanvas Play="test" />, it works, but not with the binding
I figured it out, saw another example where someone was very specific in the component they were binding to and followed that.
Added x:Name to the PlayMCanvas like below:
<drawables:PlayMCanvas x:Name="PlayMCan" Play="{Binding TheString}" />
Changed to this in codebehind:
PlayMCan.BindingContext = TheVM;
And now the binding seems to be working.
Thanks for the comments!

Xamarin forms dynamic ContentPages

I need some suggestions, help to solve any problems. I have to create a view with a dynamic count of ContentPages. I created two ViewModels, one with a logic and a stop watch and a second one to control the dynamic pages. It looks like this:
CircleViewModel:
public class CircleViewModel: ViewModelBase {
public ObservableCollection<Circle> Circles { get; set; }
private string _timeText;
public string TimeText {
get => _timeText;
set {
_timeText = value;
OnPropertyChanged();
}
// Some ICommands and methods
public async Task StartStopWatch() { ... }
}
MultiPageViewModel:
public class MultiPageViewModel : ViewModelBase {
public ObservableCollection<CircleViewModel> Pages { get; set; }
private CircleViewModel _currentPage;
public CircleViewModel CurrentPage {
get => _currentPage;
set {
_currentPage = value;
OnPropertyChanged();
}
// Some ICommands and methods
}
MultiPageView.xaml:
<ContentPage ...
BindingContext="{Binding MultiPageViewModel, Source={StaticResource Locator}}">
<Grid>
<ListView ItemsSource="{Binding CurrentPage.Circles}"/>
<Label Text="{Binding CurrentPage.Title}"/>
<Button Text="{Binding CurrentPage.TimeText}" Command="{Binding CurrentPage.StartWatchCommand}"/>
<userControls:VerticalTabView ItemsSource="{Binding MultiPageViewModel.Pages, Source={StaticResoruce Locator}}"
MoreButtonCommand="{Binding MultiPageViewModel.MoreButtonCommand, Source={StaticResource Locator}}"/>
</Grid>
</ContentPage>
VerticalTabView is a user control to change the current page.
I did'nt like the usability, so I changed the ContentPage to CarouselPage. With UWP it works fine, with Android the scrolling starts to stutter, in the output of Visual Studio appears "The application may be doing too much work on its main thread." and crashes with an OutOfMemory Exception. The CarouselPage does not work with iOS currently.
How can I improve this? I am grateful for any advice. Thank you.

Xamarin Forms Refresh Layout after Observable Collection changes

I'm having a though time trying to "refresh" one of the views where I'm using a WrapLayout. Even though I change the items inside the ObservableCollection the page does not show the changes made.
Code below (some obfuscation needed due to confidentiality issues but I think the most important part is all there). Any help would be greatly appreciated.
Thanks.
ItemCardsViewModel.cs
// INotifyPropertyChanged implemented on BaseViewModel
public class ItemCardsViewModel : BaseViewModel
{
public ObservableCollection<ItemViewModel> Items { get; set; }
public ICommand RefreshCardsCommand { get; private set; }
public Action OnItemsChanged { get; internal set; }
public ItemCardsViewModel()
{
(...)
this.RefreshCardsCommand = new Command(RefreshCards);
}
private void RefreshCards(object x)
{
this.Items = new ObservableCollection<ItemViewModel>(
this.Items.Select(x =>
{
x.IsVisible = false;
return x;
}));
OnPropertyChanged(nameof(this.Items));
if (this.OnItemsChanged != null)
OnItemsChanged();
}
(...)
}
ItemCards.xaml.cs
public partial class ItemCards : ContentPage
{
ItemCardsViewModel ViewModel => ((ItemCardsViewModel)this.BindingContext);
public ItemCards()
{
InitializeComponent();
foreach (var item in ViewModel.Items)
{
var cell = new ItemView { BindingContext = item };
CardsLayout.Children.Add(cell);
}
ViewModel.OnItemsChanged += CardsLayout.ForceLayout;
}
}
ItemCards.xaml
<?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:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
prism:ViewModelLocator.AutowireViewModel="True" (...)>
<ContentPage.Content>
<Grid>
(...)
<ScrollView Orientation="Vertical" Padding="0,5,0,5" Grid.Column="0" Grid.Row="2">
<ScrollView.Content>
<local:WrapLayout x:Name="CardsLayout" Spacing="5" HorizontalOptions="Start" VerticalOptions="Start" />
</ScrollView.Content>
</ScrollView>
</Grid>
</ContentPage.Content>
</ContentPage>
EDIT: Forgot to mention but I'm using Prism so the ViewModel is automatically wired up to the view.
EDIT 2: Just a quick update on this one... The issue persists even if I don't create a new Instance of the ObservableCollection on the RefreshCards method but rather loop through the records and set the IsVisible property one by one. Also tried to add a new ItemViewModel to the collection. Always the same result, no changes are shown on the page.

xamarin broadcast receiver access viewmodel

Notes: Xamarin 4.2.1.64, Visual Studio 2015 proff.
I have created a cross platform app that is to work on a android device that scans barcodes.
Currently When scanned the software has an optional output mode, (buffer, keyboard,clipboard and intent).
Currently using keyboard mode.
Flow
User clicks device button scanning a barcode, software attempts to dump to an input on the screen, if not it instead tabs (app is set focus to entry field on startup). When a button on the app is clicked it calls my service to query a set of data and return the results, the list of results is then updated for the user to see.
Flow i need to change
User clicks device button scanning a barcode, only this time device is set to intent and broadcasts, my app reciever picks up the broadcast, reads the barcode from the intent and calls my viewmodel to update the field with the barcode. the viewmodel will now change to detect field change and run the method accordingly.
Code so far
Portable Xaml
<?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="StockCheckApp.Views.ScannerPage"
xmlns:ViewModels="clr-namespace:StockCheckApp.ViewModels;assembly=StockCheckApp">
<Label Text="{Binding MainText}" VerticalOptions="Center" HorizontalOptions="Center" />
<StackLayout Orientation="Vertical">
<Entry x:Name="myBox" Text="{Binding UserInput, Mode=TwoWay}" />
<Button Text="Check Stock" Command="{Binding PostCommand}"/>
<ListView ItemsSource="{Binding StockList}"
HasUnevenRows="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Vertical" Padding="12,6">
<Label Text="{Binding St_Code}" FontSize="24" />
<Label Text="{Binding St_Desc}" />
<Label Text="{Binding Sl_Loc}" />
<Label Text="{Binding Sa_Datetime}" />
<Label Text="{Binding Sa_Qty}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage>
Portable Xaml Code behind
public partial class ScannerPage : ContentPage
{
public ScannerPage()
{
InitializeComponent();
BindingContext = new MainViewModel(this);
}
protected override void OnAppearing()
{
base.OnAppearing();
myBox.Focus();
}
public Entry MyBox
{
get
{
return myBox;
}
}
}
Portable Main view model
public class MainViewModel : INotifyPropertyChanged
{
ScannerPage page;
private List<Stock> _stockList;
private string _userInput;
public List<Stock> StockList
{
get { return _stockList; }
set
{
_stockList = value;
OnPropertyChanged();
}
}
public string UserInput
{
get { return _userInput; }
set
{
_userInput = value;
OnPropertyChanged();
}
}
public MainViewModel(ScannerPage parent)
{
page = parent;
page.MyBox.Focus();
}
public Command PostCommand
{
get
{
return new Command(async () =>
{
var stockService = new StockService();
StockList = await stockService.GetStockAsync(UserInput);
page.MyBox.Focus();
});
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
//use this to clear? then reset focus?
}
}
Android reciever class
[BroadcastReceiver(Enabled = true)]
[IntentFilter(new[] { "android.intent.action.bcr.newdata" })]
public class Receiver1 : BroadcastReceiver
{
public override void OnReceive(Context context, Intent intent)
{
Toast.MakeText(context, "Received intent!", ToastLength.Short).Show();
Services.StockService meh = new Services.StockService();
//MainViewModel md = new MainViewModel();
Dowork(meh, intent);
}
//this isnt correct i need to update viewmodel not call service direct!
private async void Dowork (Services.StockService meh, Intent intent)
{
string action = intent.Action.ToString();
string decodedBarcode = intent.GetStringExtra(BCRIntents.ExtraBcrString);
//now i just need to update my field in the xaml....somehow
}
}
What I'm stuck on
I step through and my code hits my breakpoints, but at this stage I need my reciver to somehow update the entry field.
I'm not familiar with Xamarin yet and I'm learning alot, so I realize this may actually be a simple answer.
Also
Am I correct in what I intend to do? recieve the barcode number and access the viewmodels "userinput" property and change it? os should I somehow access the field on the view and change it instead then allow my property changed method to carry out business logic?
You could use the Messaging Center that ships with Xamarin.Forms
https://developer.xamarin.com/guides/xamarin-forms/messaging-center/
Have your ViewModel Subscribe to the MessagingCenter for an event coming from your service, then use the event to update the property that is bound to the field. If you need a type in your PCL to map the subscribe to, create an interface for your service in the PCL that doesn't need to have any actual method contracts, then have your service implement it so you can set up your subscribe with strong types:
// in your PCL
public interface IScanReceiver
{
}
// in your android project
public class Receiver1 : BroadcastReceiver, IScanReceiver
// in your viewmodel
MessagingCenter.Subscribe<IScanReceiver>();
Alternatively, you could set up your ViewModels in a dependency service and then use the Service Locator (anti)pattern to find your ViewModel instance and update the property that is bound to the field.
MVVM Light: http://www.mvvmlight.net/ does a great job at giving you tools to do this.

What's wrong with my Xamarin.Forms project?

I'm trying to make a Xamarin.Forms project where I have a BoxView and an Entry field and a Button. I want to enter the name of a color into my Entry field, press the button, and have my BoxView change to the color that I input. Here is the code I have written till now:
Views/MainView.xaml
<?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="TestGround.MainView">
<ContentPage.Content>
<StackLayout VerticalOptions="Center">
<Label
Text="Enter a color:"
VerticalOptions="Center"
HorizontalOptions="Center"
/>
<BoxView
Color="{Binding Color}"
/>
<Entry
Text="{Binding Name}"
/>
<Button
Text="Enter"
Command="{Binding SetColorCommand}"
/>
</StackLayout>
</ContentPage.Content>
</ContentPage>
ViewModels/MainViewModel.cs
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows.Input;
using Xamarin.Forms;
namespace TestGround
{
public class MainViewModel :INotifyPropertyChanged
{
private string _color; //backing field for Greeting
public string Color //implementation for Greeting method
{
get { return _color; }
set
{
_color = value;
OnPropertyChanged ("Color"); //Notify view that change has taken place
}
}
public string Name { get; set; } //Name method for Entry field
public ICommand SetColorCommand { get; set; } //ICommand binds to buttons in XAML
public void SetColor() //Need a regular method to add to ICommand
{
Color = Name;
}
//Main VIEW MODEL
public MainViewModel ()
{
//Color = Name;
Name = "Enter color here";
SetColorCommand = new Command(SetColor); //Regular command added to ICommand
}
#region PropertyChangedRegion
public void OnPropertyChanged (string propertyName)
{
if (PropertyChanged != null)
PropertyChanged (this, new PropertyChangedEventArgs (propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
}
Here is the error I get:
Java.Lang.RuntimeException: java.lang.reflect.InvocationTargetException
I want to know if my approach is wrong and how can I go about fixing it and making this pretty simple program.
According to the BoxView Documentation, the property "Color" must actually be a color... where as you have it defined as a string named color. Your types are mixed up. It should be something like Colors.Blue.
You can use class ColorTypeConverter for change string to Color.
I 've simplified your problem to this source code
//You simplified model
public class bModel : BindableObject
{
private Color _realColor;
public Color Color
{
get { return _realColor; }
set
{
_realColor = value;
OnPropertyChanged ("Color");
}
}
public string _stringColor;
public string StringColor {
get {
return _stringColor;
}
set {
_stringColor = value;
Color = (Color)(new ColorTypeConverter ()).ConvertFrom (_stringColor);
}
}
public bModel ()
{
StringColor = "Blue";
}
}
}
//Your simplified page xaml
<?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="s2c.MyPage">
<ContentPage.Content>
<BoxView x:Name="box" Color="{Binding Color}"/>
</ContentPage.Content>
</ContentPage>
//Your simplified page csharp
public partial class MyPage : ContentPage
{
public MyPage ()
{
InitializeComponent ();
this.BindingContext = new bModel ();
}
}

Categories