MAUI add control element to GUI using MVVM - c#

I use the community tool MVVM for my current MAUI project.
I would like to dynamically add controls like an entry to the GUI during runtime. I would like to do that from the ViewModel.
Using the toolkit, it is of course very easy to provide and interact with functions and properties. Unfortunately I haven't found a way to directly access a StackLayout or something similar.
I tried giving the VerticalStackLayout property x:name (in my xaml document) a name and then accessing it. This works from the code-behind, but not from the ViewModel itself.
I expected that with in the viewModel for example my StackLayout is displayed and then I can execute the following.
stackLayout.Add(new Label { Text = "Primary colors" }));
Furthermore I tried to provide a binding to the property x:name.
x:Name="{Binding StackLayout}
In the ViewModel I then tried to provide the property.
[ObservableProperty]
VerticalStackLayout stackLayout;

To clarify: the ViewModel doesn't know about the View, but the View DOES know about the ViewModel.
Thus, the view's code behind can do what is needed.
If the View doesn't already have a property holding the viewmodel, then add to code behind:
private MyVM VM => (MyVM)BindingContext;
That defines a VM property, so you can do VM.MyDictionary[someKey] or similar.
If you need to access VM in constructor BEFORE setting BindingContext,
then edit question, to show how BindingContext is set currently.

Yes, you can use MVVM to achieve this.
A simple method is to use Bindable Layouts to achieve this.
Please refer to the following code:
1.create a viewmodel for current page
MyViewModel.cs
public class MyViewModel
{
public int index = 0;
public ObservableCollection<Data> Items { get; set; }
public ICommand AddItemCommand => new Command(addItemMethod);
private void addItemMethod(object obj)
{
index++;
Items.Add(new Data { FileName ="File " + index});
}
public MyViewModel()
{
Items = new ObservableCollection<Data>();
}
}
Data.cs
public class Data
{
public string FileName { get; set; }
}
2.MainPage.xaml
<?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:mauiapp="clr-namespace:MauiAddViewApp116"
x:Class="MauiAddViewApp116.MainPage"
x:Name="mainpage"
>
<ContentPage.BindingContext>
<mauiapp:MyViewModel></mauiapp:MyViewModel>
</ContentPage.BindingContext>
<ScrollView>
<VerticalStackLayout
Margin="10"
VerticalOptions="StartAndExpand">
<Button Text="Add item" Command="{Binding AddItemCommand}"></Button>
<StackLayout BindableLayout.ItemsSource="{Binding Items}" Orientation="Vertical">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Label HorizontalOptions="Fill" Text="{Binding FileName}" FontSize="Large" HeightRequest="38" />
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</VerticalStackLayout>
</ScrollView>
</ContentPage>

I have found a solution to my problem.
As you have advised me, I have put it around. I use the code-behind of my view to access the StackLayout.
1. MainPage.xaml
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center"
x:Name="VStackLayout">
</VerticalStackLayout>
</ScrollView>
With the property x:name I can access the VS layout from the code behind.
2. MainPage.xaml.cs
Dictionary<string, object> keyValuePairs = new Dictionary<string, object>();
public MainPage(MainPageViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
foreach (var item in viewModel.KeyValues)
{
if (item.Value == "String")
{
keyValuePairs.Add(item.Key, "");
var entry = new Entry {
Placeholder = item.Key,
ClassId = item.Key,
Text = (String)keyValuePairs.Where(k => k.Key == item.Key).First().Value
};
VStackLayout.Add(entry);
}
else if (item.Value == "Boolean")
{
keyValuePairs.Add(item.Key, true);
Label label = new Label { Text = item.Key};
var toogle = new Switch
{
IsEnabled = true,
ClassId = item.Key,
IsToggled = (Boolean)keyValuePairs.Where(k => k.Key == item.Key).First().Value
};
HorizontalStackLayout views = new HorizontalStackLayout();
views.HorizontalOptions = LayoutOptions.StartAndExpand;
views.VerticalOptions = LayoutOptions.Center;
views.Add(label);
views.Add(toogle);
VStackLayout.Add(views);
}
}
Here the Dic in the ViewModel is accessed and then the GUI is created from it.
Unfortunately, the access to the content of the elements (entries) does not work yet. I would like to see how to write the content in a Dictonary. The binding at this point does not work yet. Does anyone have an idea?

First of all, I want to answer that nothing is stopping you from passing a reference of your StackLayout as CommandParameter to your Command in the ViewModel. Write this:
[RelayCommand]
void Add(StackLayout myLayout)...
And just pass the reference in the XAML.
However, there are very few situations that justify this.
None of those situations are "to customize the GUI".
You need to learn how to use DataTemplates, DataTriggers, Styles, EventToCommandBehaviors, Gestures, ControlTemplates, Validators and ValueConvertors.
This will cover your basic needs for accessing the View and its elements.

Related

cant load data from picker on first page load but when visiting page second time picker loads as expected

I have xamarin page that shows list of products and a picker which represents type of product.
My problem is that when i start application and try to access page for the first time,
in debugging mode i can see that list i am using as ItemsSource has value,
But when page is loaded picker is greyed out and doesnt have any data.
When I leave page with picker and open it up second time, picker is loaded with data!
Here is my code!
<?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:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:Converters="clr-namespace:eProdaja.Mobile.Converters"
mc:Ignorable="d"
x:Class="Restoran.Mobile.Views.ProizvodiPage">
<ContentPage.Resources>
<ResourceDictionary>
<Converters:ImageConverter x:Key="imgConv"></Converters:ImageConverter>
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.Content>
<StackLayout>
<Picker ItemsSource="{Binding TipProizvodaList}" ItemDisplayBinding="{Binding Naziv}" SelectedItem="{Binding SelectedTipProizvoda}"></Picker>
<ListView ItemsSource="{Binding ProizvodiList}" ItemSelected="ListView_ItemSelected" >
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Padding="10" Margin="5" HorizontalOptions="CenterAndExpand" >
<Image Source="{Binding Slika, Converter={StaticResource imgConv}}" ></Image>
<Label Text="{Binding Naziv}"
d:Text="{Binding .}"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemTextStyle}"
FontSize="16" HorizontalTextAlignment="Center" HorizontalOptions="CenterAndExpand" />
<Button HorizontalOptions="Center" BorderColor="Transparent" BackgroundColor="Transparent" TextColor="OrangeRed" Text="Dodaj u košaricu"></Button>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
Code inside of xaml.cs
private ProizvodiViewModel model = null;
public ProizvodiPage()
{
InitializeComponent();
BindingContext = model = new ProizvodiViewModel();
}
protected async override void OnAppearing()
{
base.OnAppearing();
await model.Init();
}
Code inside of viewModel
public ObservableCollection<TipProizvoda> TipProizvodaList { get; set; } = new ObservableCollection<TipProizvoda>();
public async Task Init()
{
if (TipProizvodaList.Count == 0)
{
var TPList = await _tipProizvoda.Get<List<TipProizvoda>>(null);
TipProizvodaList.Clear();
TipProizvoda empty = new TipProizvoda { TipProizvodaID = 0, Naziv = "" };
TipProizvodaList.Add(empty);
foreach (var tipProizvoda in TPList)
{
TipProizvodaList.Add(tipProizvoda);
}
}
}
Over the years, I've had various problems attempting to do ANYTHING in OnAppearing that affects what is seen on screen.
There must be something Xamarin does AFTER OnAppearing, that gets the page into a valid state to receive binding changes.
One way around this limitation is to delay the work that you want done, so that OnAppearing returns before your work sets the bindings.
The "downside" of this, is that the page will appear (at first) without your work. See the two references to "activityIndicator" - those are where you control what you want user to see before your work is ready.
The "upside" of this, is that it ensures Xamarin "sees" your binding changes. (It also provides a place to do slow background work, if that is needed.)
Try this:
public partial class MyPage : ...
{
protected override void OnAppearing()
{
base.OnAppearing();
This returns immediately, allowing OnAppearing to return.
DelayWork(100, BackgroundWork, UIWork);
}
// Custom class, used to pass results from background work to UI work.
class BackgroundResult {
...
}
private void DelayWork(int milliseconds, Func<BackgroundResult> backgroundWork, Action uiWork)
{
//OPTIONAL activityIndicator.IsRunning = true;
Task.Run( () => {
// The delay ensures Xamarin page preparation has a little time, before your work begins.
// Without this delay, under some circumstances, the page might not show up as quickly.
// You might not need this.
Task.Delay(milliseconds);
// Slow work -- do nothing that affects UI.
BackgroundResult backgroundResult = BackgroundWork();
Device.BeginInvokeOnMainThread(async () => {
await uiWork(backgroundResult);
});
});
}
private BackgroundResult BackgroundWork()
{
// Slow work -- do nothing that affects UI.
...
// fill this with whatever info you need to pass to UIWork.
var backgroundResult = new BackgroundResult();
// ...
return backgroundResult;
}
private async void UIWork(BackgroundResult backgroundResult)
{
// Work that affects UI, possibly via Bindings.
await model.Init();
//OPTIONAL activityIndicator.IsRunning = false;
}
}
In your situation, you might not need BackgroundWork nor BackgroundResult. Shown for completeness.

xamarin forms how to populate picker from code behind

Evening All,
Learning Xamarin forms..attempting to add a picker with numeric values...(using https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/picker/populating-itemssource)
I have used the example on this page to get the picker populated from the view...which works fine...however I want to populate the picker from code behind...
<---XAML--->
<Picker Grid.Column="4" Grid.Row="2" ItemsSource="{Binding pickerSource}"/>
<---c#---->
var pickerList = new List<string>();
pickerList.Add("1");
pickerList.Add("2");
pickerList.Add("3");
pickerList.Add("4");
pickerList.Add("5");
pickerList.Add("6");
pickerList.Add("7");
pickerList.Add("8");
pickerList.Add("9");
pickerList.Add("10");
var pickerSource = new Picker { Title = "Quantity", TitleColor = Color.Red };
pickerSource.ItemsSource = pickerList;
Picker is appearing on app but when selected, its not populated with any values...why isnt this binding properly anyone?
Thank you
Also...as a side note if anyone is aware of a tool that contains all numeric values instead of me manually having to populate it with 1,2,3 etc..
Thanks Again
Thanks to #Jason for the reply...From here I have went with the following:
---xaml--
<Picker Grid.Column="4" Grid.Row="2" ItemsSource="{Binding pickerSource}"/>
---c#----
public List<string> pickerSource { get; set; }
public void PopulateQuantityPicker()
{
var pickerList = new List<string>();
pickerList.Add("1");
pickerList.Add("2");
pickerList.Add("3");
pickerList.Add("4");
pickerList.Add("5");
pickerList.Add("6");
pickerList.Add("7");
pickerList.Add("8");
pickerList.Add("9");
pickerList.Add("10");
pickerSource = pickerList;
this.BindingContext = this;
}
The picker is on the app, but it is not populated, it is empty.
When I Click on it I get the following:
(also the code is hitting the PopulateQuantityPicker())
here you are binding your ItemsSource to pickerSource
<Picker Grid.Column="4" Grid.Row="2" ItemsSource="{Binding pickerSource}"/>
in your code behind, you need a public property named pickerSource. You can only bind to public properties
public List<string> pickerSource { get; set }
// assign the data to your ItemsSource
pickerSource = pickerList;
// also be sure to set the BindingContext
BindingContext = this;
// this is creating a new picker named pickerSource. You have already done
// this in your XAML. This is NOT NEEDED
var pickerSource = new Picker { Title = "Quantity", TitleColor = Color.Red };
pickerSource.ItemsSource = pickerList;
if you want to do this from the code behind WITHOUT using binding, you first need to assign an x:name to your control
<Picker x:Name="myPicker" Grid.Column="4" Grid.Row="2" />
then in the code behind
myPicker.ItemsSource = pickerList;

AvalonDock MVVM Anchorable Location

I have an AvalonDock (version 3.5) in my MVVM WPF application. The binding is as follows:
<xcad:DockingManager Name="_dockingManager" Grid.Row="1"
DataContext="{Binding DockingManagerViewModel}"
DocumentsSource="{Binding Documents}"
AnchorablesSource="{Binding Anchorables}" >
So when I add a new Anchorable to the corresponding "Anchorables" collection in my view model the corresponding view shows up. Nevertheless the views always show up docked on the right side of my application. How can I control that the view is docked on the left side of my application via the view model?
I don't think you can control this in you viewmodel.
There 2 ways for controlling this.
You can restore the layout from a previously saved (default) layout whenever the
application is started for te first time or
You can setup the XAML to use an initial layout as you wish (prefered solution)
For the second option:
You can use the XAML binding in the DockingManager class to implement your requirement:
See TestApp sample for full implementation of the below snippet (just change LeftSide to BottomSide to see the effect):
<avalonDock:DockingManager Grid.Row="1">
...
<avalonDock:LayoutRoot.LeftSide>
<avalonDock:LayoutAnchorSide>
<avalonDock:LayoutAnchorGroup>
<avalonDock:LayoutAnchorable Title="AutoHide1 Content" ContentId="AutoHide1Content" IconSource="/AvalonDock.TestApp;component/Images/address-book--pencil.png" >
<TextBox Text="{Binding TestTimer, Mode=OneWay, StringFormat='AutoHide Attached to Timer ->\{0\}'}"/>
</avalonDock:LayoutAnchorable>
<avalonDock:LayoutAnchorable Title="AutoHide2 Content" ContentId="AutoHide2Content">
<StackPanel Orientation="Vertical">
<TextBox/>
<TextBox/>
</StackPanel>
</avalonDock:LayoutAnchorable>
</avalonDock:LayoutAnchorGroup>
</avalonDock:LayoutAnchorSide>
</avalonDock:LayoutRoot.LeftSide>
</avalonDock:LayoutRoot>
</avalonDock:DockingManager>
You can add a property (call it InitialPosition, or something like that) to your Anchorable view model, and implement a ILayoutUpdateStrategy to position the anchorable on the left, right or bottom side.
Add something like this to your XAML:
<xcad:DockingManager …>
…
<xcad:DockingManager.LayoutUpdateStrategy>
<local:LayoutUpdate />
</xcad:DockingManager.LayoutUpdateStrategy>
</xcad:DockingManager>
and your LayoutUpdate class:
class LayoutUpdate: ILayoutUpdateStrategy
{
static Dictionary<PaneLocation, string> _paneNames = new Dictionary<PaneLocation, string>
{
{ PaneLocation.Left, "LeftPane" },
{ PaneLocation.Right, "RightPane" },
{ PaneLocation.Bottom, "BottomPane" },
};
public bool BeforeInsertAnchorable(LayoutRoot layout, LayoutAnchorable anchorableToShow, ILayoutContainer destinationContainer)
{
if (anchorableToShow.Content is IAnchorable anch)
{
var initLocation = anch.InitialLocation;
string paneName = _paneNames[initLocation];
var anchPane = layout.Descendents()
.OfType<LayoutAnchorablePane>()
.FirstOrDefault(d => d.Name == paneName);
if (anchPane == null)
{
anchPane = CreateAnchorablePane(layout, Orientation.Horizontal, initLocation);
}
anchPane.Children.Add(anchorableToShow);
return true;
}
return false;
}
static LayoutAnchorablePane CreateAnchorablePane(LayoutRoot layout, Orientation orientation,
PaneLocation initLocation)
{
var parent = layout.Descendents().OfType<LayoutPanel>().First(d => d.Orientation == orientation);
string paneName = _paneNames[initLocation];
var toolsPane = new LayoutAnchorablePane { Name = paneName };
if (initLocation == PaneLocation.Left)
parent.InsertChildAt(0, toolsPane);
else
parent.Children.Add(toolsPane);
return toolsPane;
}
public void AfterInsertAnchorable(LayoutRoot layout, LayoutAnchorable anchorable)
{
// here set the initial dimensions (DockWidth or DockHeight, depending on location) of your anchorable
}
This code is extracted and changed a bit from the working application, with different types and names. It should probably work, but there might be a typo or other error somewhere.

Dynamically coupling XAML with code-behind

A little background information
I am learning Xamarin.Forms and am currently struggling a bit with dynamically coupling my ContentPage's XAML with my code-behind. Obviously, I am at the mercy of my complete unawareness of how Xamarin.Form's should be written, so I hope you can bare with my slight confusion.
I am developing a mobile application for Android and am using the BottomNavigationBarXF to put the navigation bar at the bottom, which is working well. Currently, I am using the example project for my learning.
The actual problem
I have created a series of ContentPage's which I would like to dynamically couple upon instantiating each new page. My ContentPage's have corresponding code-behind, which I have left untouched; e.g., I have a ContentPage named HomePage, which have this code-behind:
namespace BottomBarXFExample
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class HomePage : ContentPage
{
public HomePage()
{
InitializeComponent();
}
}
}
and this corresponding 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="BottomBarXFExample.HomePage">
<ContentPage.Content>
<StackLayout>
<Label Text="Welcome to Xamarin.Forms!"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand" />
</StackLayout>
</ContentPage.Content>
The way I go about creating pages is as follows.
string[] tabTitles = { "Me", "Trends", "Home", "Plan", "About" };
ContentPage[] pages = new ContentPage[tabTitles.Length];
for (int i = 0; i < tabTitles.Length; ++i)
{
ContentPage page = createPage(tabTitles[i]);
bottomBarPage.Children.Add(page);
}
The createPage method:
private ContentPage createPage(String title)
{
FileImageSource icon = setIcon(title);
ContentPage page = new ContentPage()
{
Title = title,
Icon = icon,
};
// should something happen here with the XAML?
return page;
}
And the setIcon method:
private FileImageSource setIcon(String title)
{
FileImageSource icon = (FileImageSource)FileImageSource.FromFile(
string.Format(
"ic_" + title.ToLowerInvariant() + ".png",
title.ToLowerInvariant()
));
return icon;
}
Using this approach I am successful in creating the bottom navigation bar. However, navigating to each page using the navigation bar, the view is "obviously" empty, because I am not linking the ContentPage to its corresponding XAML. Can this be done in code?
If I opt for instantiating each ContentPage the "right" way:
HomePage homePage = new HomePage()
{
Title = "Home",
Icon = homeIcon
};
And then add them to the navigation bar like so:
bottomBarPage.Children.Add(homePage)
I do obtain coupling between XAML and code-behind. However, I find it rather tedious, and probably also unnecessary, to do it this way.
Any suggestions?
Thank you,
Kris
Xaml pages and code behind classes are tightly coupled in xaml file with x:Class definition. Xaml pages cannot be inherited, but ContentPage classes can but I don't see that resolving your issues. If you're looking after having only one xaml page then you'd have to create the rendering logic in code behind, e.g.
public HomePage(string title)
{
InitializeComponent();
switch(title)
{
// set Binding Context to your VM
... BindingContext = titleBasedVM;
}
}
Your VM can then contain page specific data. This concept uses MVVM, which is highly recommended when using Xamarin Forms.
Also take a look at ControlTemplate for rendering generic page sections.
You shouldn't try to generate xaml dynamically as it's not supported.

Using MessagingCenter for communicating between ViewModels to Views and Navigation

In a XAML Page (A View), I have a bunch of controls like Pickers that are binded to a ViewModel, called "NegotiationVM"
For example, I have a Picker binded to one of those properties "NegotiatorSelected" which returns an object "Negotiator".
My corcern is about a pair of buttons beside of that negotiator picker, are not binded to that Negotiation ViewModel or any Command, those buttons are for Adding or Editing a Negotiator. Since I'm trying to follow MVVM pattern (with no framework yet...) I would like to know what is the best practice here.
My approach is in the First ViewModel create Commands that calls methods that send an instance of the child ViewModel to the View, so when this receive it, then navigates to the child View.
public class NegotiationVM: INotifyPropertyChanged{
public NegotiationVM{
AddNegotiatorCommand = new Command(AddNegotiator,()=> !IsBusy);
}
...
public void AddNegotiator(){
negotiatorvm = new NegotiatiorVM(_client)
MessagingCenter.Send<NegotiationVM,NegotiatorVM>(this,"NegotiatiorVM",negotiatiorvm)
}
public void EditNegotiator(){
negotiatorvm = new NegotiatiorVM(_client,_negotiatior)
MessagingCenter.Send<NegotiationVM,NegotiatorVM>(this,"NegotiatiorVM",negotiatiorvm)
}
}
So in NegotiationView I do this:
<!-- More controls binded to NegotiationVM -->
...
<!-- -->
<Picker x:Name="pickerNegotiator" Title="Select..." ItemsSource="{Binding Negotiatorsofthisnegotiation}" SelectedItem="{Binding SelectedNegotiator}" />
<Button x:Name="btnAddNegotiator" Text="➕" BackgroundColor="#3276b1" TextColor="White" HorizontalOptions="End" WidthRequest="35" Command="{Binding AddNegotiatorCommand}" />
<Button x:Name="btnEditNegotiator" Text="📝" BackgroundColor="#3276b1" TextColor="White" HorizontalOptions="End" WidthRequest="35" Command="{Binding EditNegotiatorCommand}" />
<!-- ... -->
and in code-behind:
...
public NegotiationView(){
negotiationvm = new NegotiationVM();
this.BindingContext = negotiationvm;
MessagingCenter.Subscribe<NegotiationVM,NegociatorVM>(this,"NegotiatorVM",AddEditNegotiator);
}
...
void AddEditNegotiator(NegotiationVM arg1, NegotiatorVM vm)
{
NegotiatorPage negotiatorPage = new NegotiatorPage()
{
BindingContext = vm
};
Navigation.PushAsync(negotiatorPage);
}
So using MessagingCenter to communicate a ViewModel to a View in order to bind it to a child view and then Navigate to its view, is this a good practice considering that I'm not using any MVVM framework? Is this approach is making ViewModel from View decoupled enough? I read some comments that using MessagingCenter is not a good idea.

Categories