I have a MyPage with a CarouselView and two buttons below it. The buttons are for navigating between ContentView views inside the CarouselView:
[CarouselView]
[Prev] [Next]
ContentViewA and ContentViewB are inside of CarouselView
The MyPageViewModel has commands for the previous and next buttons:
class MyPageViewModel : BindableBase
{
public ICommand ShowPrevCommand { get; private set;}
public ICommand ShowNextCommand { get; private set;}
}
How do I implement the commands to make the CarouselView show the views?
According to documentation here
In Prism, the concept of navigating to a View or navigating to a
ViewModel does not exist. Instead, you simply navigate to an
experience, or a unique identifier, which represents the target view
you wish to navigate to in your application
so I was thinking I could use INavigationService.
I was thinking I could implement my own NavigationService and on NavigateAsync I could check if current page is MyPage. If it is, I could set the view inside of CarouselView to the view based on the navigation name parameter.
I am however not sure how to implement and override Prism's navigation service.
Can Prism for Xamarin Forms do something like this?
It does not have to be that complicated. CarouselView has the bindable property Position, which you can bind to a property of your viewmodel
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:forms="clr-namespace:Xamarin.Forms;assembly=Xamarin.Forms.CarouselView">
<ContentPage.Content>
<forms:CarouselView Position="{Binding CarouselPosition}">
<!-- whatever to display in the CarouselView -->
</form:CarouselView>
</ContentPage.Content>
</ContentPage>
In your viewmodel you can implement th navigation the following way:
class MyPageViewModel : BindableBase
{
public MyPageViewModel()
{
ShowPrevCommand = new Command(ShowPrev);
ShowNextCommand = new Command(ShowNext);
}
public ICommand ShowPrevCommand { get; private set;}
public ICommand ShowNextCommand { get; private set;}
void OnShowPrev()
{
CarouselPosition--;
}
void OnShowNext()
{
CarouselPosition++;
}
public int CarouselPosition
{
get => _carouselPosition;
set
{
if(value == _carouselPosition)
{
return;
}
this._carouselPosition = value;
PropertyChanges?.Invoke(this, new PropertyChangedEventArgs(CarouselPosition));
}
}
}
Just to get the gist. Of course you'll have to handle cases like overflows (i.e. CarouselPosition exceeds the number of views in the carousel), etc.
Related
Update: I've updated this a bit to remove the reference to the error. #michal-diviš gave the correction solution to that. However, my larger issue still remains.
I'm new to Xamarin and trying to learn by making a simple email client. I'm trying to set a property on a ContentPage I have created.
The Setup
The MainPage simply has a grid with two columns; the left side features an CollectionView of the inbox, the right side is my custom ContentPage MessageDisplayView. When an email is clicked in the CollectionView, the CurrentMessage property on the MainPageViewModel is updated to the selected item.
The Issue
I'm trying to bind the property MessageDisplayView.Message to the MainPageViewModel.CurrentMessage property, but the contentpage never updates. I've tried with and without BindableProperty, as well as other ideas found while searching Google and Stackoverflow.
The Question
How do I handle setting and updating a property that I would like to live with the ContentPage?
The Code
MainPage.xaml
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:c="Microsoft.Toolkit.Uwp.UI.Controls"
xmlns:vm="clr-namespace:Project.ViewModel"
xmlns:view="clr-namespace:Project.View"
xmlns:fa="clr-namespace:FontAwesome"
x:Class="Project.MainPage">
<ContentPage.BindingContext>
<vm:MainPageViewModel/>
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<ResourceDictionary Source="ResourceDictionaries/EmailResourceDictionary.xaml"/>
</ResourceDictionary>
</ContentPage.Resources>
<Grid x:Name="MainPageGrid">
<!-- other xaml code -->
<view:MessageDisplayView
x:Name="MyDisplayView"
Grid.Column="1"
Message="{Binding CurrentMessage}" <!-- Error -->
/>
</Grid>
</ContentPage>
MainPageViewModel.cs
using MimeKit;
using Project.EmailLogic;
using System.Collections.ObjectModel;
using System.Windows.Input;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Project.ViewModel
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public class MainPageViewModel: ObservableObject
{
private MimeMessage currentMessage;
public MimeMessage CurrentMessage
{
get => currentMessage;
set => SetProperty(ref currentMessage, value, nameof(MessageDisplayView.Message));
}
public MainPageViewModel()
{
}
}
}
MessageDisplayView.xaml
<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:view="clr-namespace:Project.View"
xmlns:vm="clr-namespace:Project.ViewModel"
x:DataType="view:MessageDisplayView"
xmlns:fa="clr-namespace:FontAwesome"
x:Class="Project.View.MessageDisplayView">
<ContentView.Content>
<Grid>
<!-- Various standard xaml things, for example... -->
<!-- Subject Line -->
<Label x:Name="SubjectLine"
Grid.Row="1"
Text="{Binding Message.Subject}"
/>
</Grid>
</ContentView.Content>
</ContentView>
MessageDisplayView.xaml.cs
using MimeKit;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Project.View
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MessageDisplayView : ContentView
{
private MimeMessage message;
public MimeMessage Message
{
get
{
return (MimeMessage)GetValue(MessageProperty);
}
set
{
SetValue(MessageProperty, value);
BodyHtmlViewSource.Html = Message.HtmlBody;
}
}
public BindableProperty MessageProperty =
BindableProperty.Create(nameof(Message), typeof(MimeMessage), typeof(MessageDisplayView));
public HtmlWebViewSource BodyHtmlViewSource { get; set; }
public MessageDisplayView()
{
InitializeComponent();
}
}
}
The problem was the BindableObject was not hearing the notifications of the property changing.
The solution was to add the OnPropertyChanged method to the code behind of the ContentView, not the ContentPageViewModel.
This "solution" correctly updates the property in the code, but it does not update the xaml/UI. I think this might a separate issue.
This confused me at first, when #michal-diviš pointed out the OnPropertyChanged calls, as I thought I was suppose to wire up the event subscription myself in the ContentView code behind. But after stumbling across this article, I realized that the method was required elsewhere.
I feel like a major issue is that there isn't a lot of information about passing data or properties between elements/UserControls/ContentPages, etc. Over the last two days, I've read and watched a fair amount on BindableProperties, but seen very little use of OnPropertyChanged or updating the properties from elsewhere. Perhaps I'm missing the places where it's talked about, or maybe it's more easy or obvious than I realize, but in hindsight, this seems like something that should have been mentioned in every BindableProperty 101.
Beyond the official documentation of course, if anyone knows a good article or video going over sharing/binding/updating properties between classes/views/whatever, I'd love to check that out.
Here's an example of the final, working code:
public partial class MessageDisplayView : ContentView
{
public MimeMessage Message
{
get
{
return (MimeMessage)GetValue(MessageProperty);
}
set
{
SetValue(MessageProperty, value);
}
}
public static BindableProperty MessageProperty =
BindableProperty.Create(nameof(Message), typeof(MimeMessage), typeof(MessageDisplayView), new MimeMessage(),
BindingMode.TwoWay);
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == MessageProperty.PropertyName)
{
if(Message != null)
{
// Update ContentView properties and elements.
}
}
}
Thank you again to #michal-diviš for your help!
Fix
It's the BindableProperty definition!
You have (in the MessageDisplayView.xaml.cs):
public BindableProperty MessageProperty = BindableProperty.Create(nameof(Message), typeof(MimeMessage), typeof(MessageDisplayView));
you need to make it static readonly like this:
public static readonly BindableProperty MessageProperty = BindableProperty.Create(nameof(Message), typeof(MimeMessage), typeof(MessageDisplayView));
Usage of INotifyPropertyChanged
The CurrentMessage property in your MainPageViewModel seems to be the problem. You've created it as a BindableProperty, however, that's meant to be used by user controls, not view models.
What you need in the view model is to implement the INotifyPropertyChanged interface and invoke the PropertyChanged event in the property setter. That is done so the UI will update itseld whenever the CurrentMessage property changes.
Tweak your MainViewModel.cs like this:
using MimeKit;
using Project.EmailLogic;
using Xamarin.Forms;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Project.ViewModel
{
public class MainPageViewModel : INotifyPropertyChanged
{
private MimeMessage currentMessage;
public MimeMessage CurrentMessage
{
get => currentMessage;
set {
currentMessage = value;
OnPropertyChanged(nameof(CurrentMessage))
};
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
In this example, I've implemented the INotifyPropertyChanged directly in you view model, but a better way to do it is to inherit from a base class that already has that implemented, like this one: ObservableObject from James Montemagno's MVVM Helpers library. The resulting view model would look like this:
using MimeKit;
using Project.EmailLogic;
using MvvmHelpers;
namespace Project.ViewModel
{
public class MainPageViewModel : ObservableObject
{
private MimeMessage currentMessage;
public MimeMessage CurrentMessage
{
get => currentMessage;
set => SetProperty(ref currentMessage, value);
}
}
}
EDIT:
Lately I've been using the CommunityToolkit.Mvvm library instead of Refactored.MvvmHelpers as it's more updated and feature rich.
I have a xamarin forms app.
There are 2 classes with data, one of the pages is filling the data.
The problem is: I'm creating new view, that should use data from both classes.
The only way i'm familiar with is to set a class as a bindingContext to pass data between pages, and it's working fine with ONE class, because apparently there couldn't be 2 bindingContext at the same time.
EXAMPLE:
1st class (all the classes are filled on the previous page. just accept that they are filled)
public class Buildings : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _id;
public string Id
{
get { return _id; }
set
{
_id = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Id"));
}
}
}
2nd class
public class Flats : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _num;
public string Num
{
get { return _num; }
set
{
_num = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Num"));
}
}
}
new view:
public partial class HouseView
{
private Flats _flats;
private Buildings _buildings;
public HouseView()
{
InitializeComponent();
}
private void HouseView_OnBindingContextChanged(object sender, EventArgs e)
{
var building = BindingContext as Building;
//var flat = BindingContext as Flat;
//_flat = flat;
_building = building;
var buildingInfo = await Rest.GetHouseInfo(_building.Id, _flat.Num); //function that will return info on a current house;
// rest code
}
}
Maybe there is no need for binding context, because i'm just passing the parameters, not changing them in a view? I guess the solution can be pretty simple, and i cant figure it out....
What you are missing is understanding the concept of ViewModel, and it's relation with the views.. In this case what you need is a 3rd class (ViewModel) that handles your 2 previous class:
public class HouseViewModel : INotifyPropertyChanged
{
public Flats Flats { get; set; }
private Buildings Buildings { get; set; }
}
Also using OnBindingContextChanged is just messy and will take some performance from your app .. try to prepare your data before on your VM, so the view knows as little as possible in how to get/handle data.
There is simple way to transfer data between pages in Xamarin forms.
Add new class to the main project called Transporter.cs, and this class should be static.
Inside this class, add the variables to transfer data between other pages; then you can simply access any variable by using Transporter.Variable.
Example:
public static Transporter
{
public static string x;
}
> Now, in each page, you can simply access (set or get) the value:
Transporter.x=MyName.Text;
>In another page:
MySecondName.Text=Transporter.x;
Note: MyName is an entry field in the first page, and MySecondName is an entry field in the second page.
Also, you can define any type of variables like (Lists, int, object... etc).
I am using the standard pivot template in my WP7 app.
I have the MainViewModel class defined with a few extra properties:
public class MainViewModel : INotifyPropertyChanged
{
...
private MyClass selectedKey_m;
public MyClass SelectedKey
{
get
{
...
}
set
{
if (value != this.selectedKey_m)
{
this.selectedKey_m = value;
NotifyPropertyChanged("SelectedKey");
}
}
}
}
The App class has a view model instance:
private static MainViewModel viewModel = null;
public static MainViewModel ViewModel
{
get
{
// Delay creation of the view model until necessary
if (viewModel == null)
viewModel = new MainViewModel();
return viewModel;
}
}
My MainPage.xaml.cs sets the DataContext:
DataContext = App.ViewModel;
From here, I can set up two way bindings on ListBoxes and I know it works because if I put a breakpoint on the SelecetdKey property in my viewmodel I can see the setter get called.
My problem is that I have my own user control, with a bindable property, bound to the SelectedKey property of the view model, but the property in my user control never gets set when the viewmodel gets updated and I can't figure out why.
Here is my user control:
public partial class MyUserControl : UserControl
{
public static readonly DependencyProperty SelectedKeyProperty = DependencyProperty.Register(
"SelectedKey", typeof(MyClass), typeof(MyUserControl), new PropertyMetadata(null));
public MyClass SelectedKey
{
get { return (MyClass)this.GetValue(SelectedKeyProperty); }
set { this.SetValue(SelectedKeyProperty, value); }
}
}
And here is the xaml in my main page:
<local:MyUserControl x:Name="myUC" SelectedKey="{Binding Path=SelectedKey}">
I would expect that the setter for the SelectedKey property of my user control to get called when the SelectedKey property of the view model gets changed, but it doesn't.
I've also tried setting the datacontext of my user control in the xaml:
DataContext="{Binding Path=App.ViewModel}"
The debugger does not step into the setter, don't know why.
Try adding a callback invoked on property value changes :
public static readonly DependencyProperty SelectedKeyProperty = DependencyProperty.Register(
"SelectedKey", typeof(MyClass), typeof(MyUserControl), new PropertyMetadata(MyPropertyChanged));
private static void MyPropertyChanged( object sender, DependencyPropertyChangedEventArgs args)
{
}
Solved. I had to add the static method as ptauzen suggested, but also remove the DataContext binding statement from my xaml :
DataContext="{Binding Path=App.ViewModel}"
Because the MainPage sets the datacontext in the constructor, so because my user control is a child of the main page, it inherits the data context. All I needed was to ensure the binding of my user controls properties were set up:
SelectedKey="{Binding SelectedKey}"
I'm once again in WPF binding hell :) I have a public class (Treatment) as follows:
public class Treatment()
{
...
public Ticker SoakTimeActual;
...
}
Within Ticker is a Dependency Property:
public class Ticker : FrameworkElement
{
// Value as string
public static readonly DependencyProperty DisplayIntervalProperty = DependencyProperty.Register("DisplayInterval", typeof(string), typeof(Ticker), null);
public string DisplayInterval
{
get { return (string)GetValue(DisplayIntervalProperty); }
set { SetValue(DisplayIntervalProperty, value); }
}
...
}
In my app, a single Treatment object is created and is meant to be easily accessible in XAML (in app.xaml ):
<Application.Resources>
<ResourceDictionary>
<u:Treatment
x:Key="currentTreatment" />
</ResourceDictionary>
</Application.Resources>
Now, I need to bind to the DisplayInterval dependency property of SoakTimeActual to display this text in its current state. Here is my attempt, which doesn't work:
<TextBlock
Text="{Binding Source={StaticResource currentTreatment}, Path=SoakTimeActual.DisplayInterval}"/>
This, of course, compiles ok, but will not display anything. I'm assuming I've made a mistake with change notification or DataContext or both.
Any insight is appreciated!
WPF binding only operates on properties, not fields.
Therefore, you need change your SoakTimeActual field to a property, like this:
public class Treatment
{
...
public Ticker SoakTimeActual { get; set; }
...
}
I am trying to implement a mvvm design pattern for xbap application But unable to carry out simple text binding.
Following is the definition of my DemoViewModel.cs,
class DemoViewModel : INotifyPropertyChanged
{
string name;
public event PropertyChangedEventHandler PropertyChanged;
public string Name
{
get
{
return name;
}
set
{
name = value;
OnPropertyChanged("Name");
}
}
public DemoViewModel()
{
Name = "test";
}
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I am binding the view to viewmodel using code behind of view,
public DemoView()
{
InitializeComponent();
DataContext = new DemoViewModel();
}
Following is the binding definition for text box present in view,
I appears that you have everything hooked up correctly. During execution, take a look at you 'Output' window and see if it gives you any warnings on you Binding. Also, try to simplify your xaml a bit to the following and see if this helps:
<TextBox Text="{Binding Name, Mode=TwoWay}"/>
Based on your comment, to JSPrang's answer, I know whats wrong =)
XBAP is missing permissions to use reflection, and can therefore only bind to public classes, unless run in full trust.