Load dynamic controls using Xamarin MVVM - xaml

I am developing an application in Xamarin Forms using MVVM where I have this JSON:
[{
type: 'text',
title: 'Name'
value: 'John',
width: 50
},
{
type: 'radio'
Source: ['Male', 'Female']
value: 'Male',
width: 100
},
{
type: 'checkbox'
title: 'Married'
value: false,
width: 100
}]
Based on this JSON, I want to load controls (TextBox, RadioButton, and CheckBox in this example). I tried to find a solution but I couldn't get succeed.
This is a dynamic JSON retrieved from the database. It could be with different controls also.
Is there a way to implement this?

Hi #confusedDeveloper,
Jason's answer works well while not considering MVVM pattern. However with MVVM it needs a little more code.
Dynamically changing of a view's content can be achieved in MVVM pattern by simply creating a view component deriving from ContentView with a BindableProperty and handling the changes to the bindable property.
Here I have created a View component ChangingView
In Xaml of the View Component assign a content view to the ComponentView
<ContentView.Content>
<ContentView Grid.Row="1" x:Name="mainView">
<ContentView.Content>
<Label BackgroundColor="Gray" Text="Placeholder"/>
</ContentView.Content>
</ContentView>
</ContentView.Content>
In the Xaml.cs of the ComponentView create BindableProperty and handle its changes.
// Fetch the required parameter from JSON and bind it to this property
public string ViewType
{
get { return (string)GetValue(ViewTypeProperty); }
set
{
SetValue(ViewTypeProperty, value);
}
}
//Notice the OnViewChanged method subscription
public static readonly BindableProperty ViewTypeProperty = BindableProperty.Create("ViewType", typeof(string), typeof(ChangingView), "placeholder", BindingMode.Default, null, OnViewChanged);
This static method is call a method similar to code in #Jason's answer
private static void OnViewChanged(BindableObject bindable, object oldvalue, object newValue)
{
var viewControl = (bindable as ChangingView);
if (viewControl != null)
{
viewControl.ChangeView((string)newValue);
}
}
private void ChangeView(string viewType)
{
if(viewType == "Button")
{
this.mainView.Content = new Button()
{
BackgroundColor= Color.Red,
Text = "Button"
};
}
else if(viewType == "Label")
{
this.mainView.Content = new Label()
{
BackgroundColor= Color.Green,
Text = "Label"
};
}
}
Finally use the component in Xaml of the Page where you require this ChangingView, The DisplayControl is the string derived from the required JSON parameter (Done in ViewModel, I leave this up to you).
<component:ChangingView ViewType="{Binding DisplayControl}"/>
Hope it fits your scenario. Comment if further information is required.
Thanks,

there are lots of ways to tackle this - one approach would be to do something like this
foreach(var c in json)
{
switch(c.type) {
case "text":
var c = new Entry() { ... };
myLayout.Children.Add(c);
break;
case "checkbox":
var c = new Checkbox() { ... };
myLayout.Children.Add(c);
break;
case ...
}
}

Related

Method that expands into tag-helper

I have the following method in a cshtml file. It simply expands into two label elements. The first is a plain label element. The second however, uses a tag helper:
async Task field(string str)
{
<label for="#str">#str</label>
<label asp-for="#str">#str</label>
}
Here's how I have it defined in the cshtml file along with calling it once:
#{
{
async Task field(string str)
{
<label for="#str">#str</label>
<label asp-for="#str">#str</label>
}
await field("abc");
}
}
If I 'view source' on the result, I see the following:
<label for="abc">abc</label>
<label for="str">abc</label>
Note that the #str argument was properly passed and used in the first case but was not in the second case. So it seems that there's an issue in passing the argument to the tag-helper variant here.
Any suggestions on how to resolve this?
In my opinion, the argument has been passed the tag-helper variant successfully. But the the label asp-for attribute will be rendered as the for attribute with asp-for ModelExpression's name value(str) not the value ModelExpression's model(abc).
According to the label taghelper source codes, you could find the tag helper will call the Generator.GenerateLabel method to generate the label tag html content.
The Generator.GenerateLabel has five parameters, the third parameter expression is used to generate the label's for attribute.
var tagBuilder = Generator.GenerateLabel(
ViewContext,
For.ModelExplorer,
For.Name,
labelText: null,
htmlAttributes: null);
If you want to show the str value for the for attribute, you should create a custom lable labeltaghelper.
More details, you could refer to below codes:
[HtmlTargetElement("label", Attributes = "asp-for")]
public class ExtendedAspForTagHelper:LabelTagHelper
{
public ExtendedAspForTagHelper(IHtmlGenerator generator)
: base(generator)
{
}
public override int Order => -10000;
//public override void Process(TagHelperContext context, TagHelperOutput output)
//{
// base.Process(context, output);
// if (!output.Attributes.TryGetAttribute("maxlength", out TagHelperAttribute maxLengthAttribute))
// {
// return;
// }
// var description = $"Only <b>{maxLengthAttribute.Value}</b> characters allowed!";
// output.PostElement.AppendHtml(description);
//}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (output == null)
{
throw new ArgumentNullException(nameof(output));
}
var tagBuilder = Generator.GenerateLabel(
ViewContext,
For.ModelExplorer,
For.Model.ToString(),
labelText: null,
htmlAttributes: null);
if (tagBuilder != null)
{
output.MergeAttributes(tagBuilder);
// Do not update the content if another tag helper targeting this element has already done so.
if (!output.IsContentModified)
{
// We check for whitespace to detect scenarios such as:
// <label for="Name">
// </label>
var childContent = await output.GetChildContentAsync();
if (childContent.IsEmptyOrWhiteSpace)
{
// Provide default label text (if any) since there was nothing useful in the Razor source.
if (tagBuilder.HasInnerHtml)
{
output.Content.SetHtmlContent(tagBuilder.InnerHtml);
}
}
else
{
output.Content.SetHtmlContent(childContent);
}
}
}
}
}
Improt this taghelper in _ViewImports.cshtml
#addTagHelper *,[yournamespace]
Result:

How to prepopulate Autocomplete SelectedItem

From my previous post, it helped be to determine how to bind to selecteditems, How to bind to autocomplete selecteditem with ObservableCollection But now I'm trying to enhance that logic.
I'm trying to have items preselected when my View is initialized. I've tried multiple options, but I can't seem to get items preselected. May I get some assistance. My current code below
Keyword Class
public class Keyword : ObservableObject
{
private string _value;
public string Value
{
get { return _value; }
set { SetProperty(ref _value, value); }
}
}
ViewModel
private ObservableCollection<object> _selectedKeywords = new ObservableCollection<object>();
private ObservableCollection<Keyword> _keywords = new ObservableCollection<Keyword>();
public TestViewModel()
{
Keywords = new ObservableCollection<Keyword>()
{
new Keyword { Value = "Apples" },
new Keyword { Value = "Bananas" },
new Keyword { Value = "Celery" }
};
SelectedKeywords = new ObservableCollection<object>(Keywords.Where(x => x.Value == "Apples"));
}
public ObservableCollection<object> SelectedKeywords
{
get { return _selectedKeywords; }
set { SetProperty(ref _selectedKeywords, value); }
}
public ObservableCollection<Keyword> Keywords
{
get { return _keywords; }
set { SetProperty(ref _keywords, value); }
}
View
<autocomplete:SfAutoComplete MultiSelectMode="Token"
HorizontalOptions="FillAndExpand"
VerticalOptions="EndAndExpand"
TokensWrapMode="Wrap"
Text="{Binding Keyword, Mode=TwoWay }"
IsSelectedItemsVisibleInDropDown="false"
Watermark="Add..."
HeightRequest="120"
SelectedItem="{Binding SelectedKeywords}"
DataSource="{Binding Keywords}">
</autocomplete:SfAutoComplete>
We have prepared sample from your code snippet and you have missed to add DisplayMemberPath property in the code snippet. Please find the sample from below location.
http://www.syncfusion.com/downloads/support/directtrac/general/ze/AutoCompleteSample-270923957.zip
Note: I work for Syncfusion.
Regards,
Dhanasekar
To make it preselected in your View Model set a value to the binding that you have binded on your View basically assign a value to SelectedKeywords
Something like:
SelectedKeywords = Keywords.FirstOrDefault();
You might need two-way binding not sure cause never used this control:
SelectedItem="{Binding SelectedKeywords, Mode=TwoWay}"

Reuse xamarin.forms xaml contentpage data throughout the app

I got a bit stuck on a problem and I was wondered if it's possible you can make a video on my problem it seems may others may have the same problem but, I been looking online but no real straight forward solution to the issue. The problem is, for instance, I have a stack layout in a content page page1.xaml and I added labels to that page 4 of them and I set properties for those labels setters and getters. however, if I make page2.xaml how would I be able to move the labels along with the data to page2.xaml basically reuse the data/labels declared from page1.xaml to page2.xaml throughout the app? here is what I have so far.
<StackLayout>
<StackLayout x:Name = "CustomerOrderStackLayout" Orientation = "Horizontal" HorizontalOptions = "Center" Padding = "20" >
<Label x:Name ="CustomerOrderNumberLabel" HorizontalOptions = "Center" />
</StackLayout>
<StackLayout Orientation = "Vertical" Padding = "20" >
<Label x:Name ="CustomerFirstNameLabel"
VerticalOptions = "Start" />
<Label x:Name ="CustomerLastNameLabel"
VerticalOptions = "Start" />
<Label x:Name ="CustomerAddressLabel"
VerticalOptions = "Start" />
<Label x:Name ="CustomerZipCodeLabel"
VerticalOptions = "Start"/>
<Label x:Name ="CustomerPhoneNumberLabel"
VerticalOptions = "Start"/>
</StackLayout>
</StackLayout>
</ContentPage>
public partial class CustomerInfoContentV : ContentPage
{
public CustomerInfoContentV()
{
InitializeComponent();
orderNum = "N";
FirstName = "Nl";
LastName = "Jk";
Address = "203030 drive";
ZipCode = "77088";
PhoneNumber = "833-223-2222";
}
public string orderNum
{
get
{
return CustomerOrderNumberLabel.Text;
}
set
{
CustomerOrderNumberLabel.Text = value;
}
}
public string FirstName
{
get
{
return CustomerFirstNameLabel.Text;
}
set
{
CustomerFirstNameLabel.Text = value;
}
}
public string LastName
{
get
{
return CustomerLastNameLabel.Text;
}
set
{
CustomerLastNameLabel.Text = value;
}
}
public string Address
{
get
{
return CustomerAddressLabel.Text;
}
set
{
CustomerAddressLabel.Text = value;
}
}
public string ZipCode
{
get
{
return CustomerZipCodeLabel.Text;
}
set
{
CustomerZipCodeLabel.Text = value;
}
}
public string PhoneNumber
{
get
{
return CustomerPhoneNumberLabel.Text;
}
set
{
CustomerPhoneNumberLabel.Text = value;
}
}
}
I want to know is there a way to take this what i have and reuse it on a whole different contentpage thats my issue i want to take this what i created and reuse it through out the app is this possible? can anyone lead me in the right direction! please thanks :)
If I am understanding you correct. You can use templates. https://developer.xamarin.com/guides/xamarin-forms/templates/control-templates/creating/ . For my projects I just create a code based template and render it whenever I want it and wherever I want them.

How to set TextColor Xamarin.Forms TableSection?

I am a fan of doing as much as possible in xaml, I have aTableView` with a TableSection.
<TableView Intent="Menu">
<TableRoot>
<TableSection Title="Test Section" TextColor="#FFFFFF">
<TextCell Text="Test Item" TextColor="#FFFFFF"/>
</TableSection>
</TableRoot>
</TableView>
For TextCell TextColor="#FFFFFF" seems to work, however whenever I use this attribute on a TableSection I get this:
An unhandled exception occurred.
Is it possible to change the color of the TableSection with xaml?
Custom Renderers! I have two blog posts on this here:
Android:Xamarin.Forms TableView Section Custom Header on Android
iOS: Xamarin.Forms TableView Section Custom Header on iOS
Basically, create a custom view that inherits TableView, then custom renderers that implement custom TableViewModelRenderer. From there you can override methods to get the header view for the section title.
Here's what that might look like for Android:
public class ColoredTableViewRenderer : TableViewRenderer
{
protected override TableViewModelRenderer GetModelRenderer(Android.Widget.ListView listView, TableView view)
{
return new CustomHeaderTableViewModelRenderer(Context, listView, view);
}
private class CustomHeaderTableViewModelRenderer : TableViewModelRenderer
{
private readonly ColoredTableView _coloredTableView;
public CustomHeaderTableViewModelRenderer(Context context, Android.Widget.ListView listView, TableView view) : base(context, listView, view)
{
_coloredTableView = view as ColoredTableView;
}
public override Android.Views.View GetView(int position, Android.Views.View convertView, ViewGroup parent)
{
var view = base.GetView(position, convertView, parent);
var element = GetCellForPosition(position);
// section header will be a TextCell
if (element.GetType() == typeof(TextCell))
{
try
{
// Get the textView of the actual layout
var textView = ((((view as LinearLayout).GetChildAt(0) as LinearLayout).GetChildAt(1) as LinearLayout).GetChildAt(0) as TextView);
// get the divider below the header
var divider = (view as LinearLayout).GetChildAt(1);
// Set the color
textView.SetTextColor(_coloredTableView.GroupHeaderColor.ToAndroid());
textView.TextAlignment = Android.Views.TextAlignment.Center;
textView.Gravity = GravityFlags.CenterHorizontal;
divider.SetBackgroundColor(_coloredTableView.GroupHeaderColor.ToAndroid());
}
catch (Exception) { }
}
return view;
}
}
}
And the on iOS:
public class ColoredTableViewRenderer : TableViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<TableView> e)
{
base.OnElementChanged(e);
if (Control == null)
return;
var coloredTableView = Element as ColoredTableView;
tableView.WeakDelegate = new CustomHeaderTableModelRenderer(coloredTableView);
}
private class CustomHeaderTableModelRenderer : UnEvenTableViewModelRenderer
{
private readonly ColoredTableView _coloredTableView;
public CustomHeaderTableModelRenderer(TableView model) : base(model)
{
_coloredTableView = model as ColoredTableView;
}
public override UIView GetViewForHeader(UITableView tableView, nint section)
{
return new UILabel()
{
Text = TitleForHeader(tableView, section),
TextColor = _coloredTableView.GroupHeaderColor.ToUIColor(),
TextAlignment = UITextAlignment.Center
};
}
}
}
According to the official documentation for TableSection you are out of luck. I'm afraid you would have to write a custom renderer for a subclass of the TableSection class and expose an extra property of type Xamarin.Forms.Color. Then you would be able to set the color from XAML.
You can set this color in the base theme (may apply to other widgets too)
In /Resources/values/styles.xml
<style name="MainTheme.Base" parent="Theme.AppCompat.Light">
<item name="colorAccent">#434857</item>
For Individual Section Titles, the TextColor property now seems to work correctly:
<TableView Intent="Settings">
<TableRoot>
<TableSection Title="App Settings" TextColor="Red">

Bind to Xamarin.Forms.Maps.Map from ViewModel

I'm working on a Xamarin.Forms app using a page that displays a map.
The XAML is:
<maps:Map x:Name="Map">
...
</maps:Map>
I know that the map can be accessed from the page's code-behind like this:
var position = new Position(37.79762, -122.40181);
Map.MoveToRegion(new MapSpan(position, 0.01, 0.01));
Map.Pins.Add(new Pin
{
Label = "Xamarin",
Position = position
});
But because this code would break the app's MVVM architecture, I'd rather like to access the Map object from my ViewModel, not directly from the View/page - either using it directly like in the above code or by databinding to its properties.
Does anybody know a way how this can be done?
If you don't want to break the MVVM pattern and still be able to access your Map object from the ViewModel then you can expose the Map instance with a property from your ViewModel and bind to it from your View.
Your code should be structured like described here below.
The ViewModel:
using Xamarin.Forms.Maps;
namespace YourApp.ViewModels
{
public class MapViewModel
{
public MapViewModel()
{
Map = new Map();
}
public Map Map { get; private set; }
}
}
The View (in this example I'm using a ContentPage but you can use whatever you like):
<?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="YourApp.Views.MapView">
<ContentPage.Content>
<!--The map-->
<ContentView Content="{Binding Map}"/>
</ContentPage.Content>
</ContentPage>
I didn't show how, but the above code snipped can only work when the ViewModel is the BindingContext of your view.
What about creating a new Control say BindableMap which inherits from Map and performs the binding updates which the original Map lacks internally. The implementation is pretty straightforward and I have included 2 basic needs; the Pins property and the current MapSpan. Obviously, you can add your own special needs to this control. All you have to do afterward is to add a property of type ObservableCollection<Pin> to your ViewModel and bind it to the PinsSource property of your BindableMap in XAML.
Here is the BindableMap:
public class BindableMap : Map
{
public BindableMap()
{
PinsSource = new ObservableCollection<Pin>();
}
public ObservableCollection<Pin> PinsSource
{
get { return (ObservableCollection<Pin>)GetValue(PinsSourceProperty); }
set { SetValue(PinsSourceProperty, value); }
}
public static readonly BindableProperty PinsSourceProperty = BindableProperty.Create(
propertyName: "PinsSource",
returnType: typeof(ObservableCollection<Pin>),
declaringType: typeof(BindableMap),
defaultValue: null,
defaultBindingMode: BindingMode.TwoWay,
validateValue: null,
propertyChanged: PinsSourcePropertyChanged);
public MapSpan MapSpan
{
get { return (MapSpan)GetValue(MapSpanProperty); }
set { SetValue(MapSpanProperty, value); }
}
public static readonly BindableProperty MapSpanProperty = BindableProperty.Create(
propertyName: "MapSpan",
returnType: typeof(MapSpan),
declaringType: typeof(BindableMap),
defaultValue: null,
defaultBindingMode: BindingMode.TwoWay,
validateValue: null,
propertyChanged: MapSpanPropertyChanged);
private static void MapSpanPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var thisInstance = bindable as BindableMap;
var newMapSpan = newValue as MapSpan;
thisInstance?.MoveToRegion(newMapSpan);
}
private static void PinsSourcePropertyChanged(BindableObject bindable, object oldvalue, object newValue)
{
var thisInstance = bindable as BindableMap;
var newPinsSource = newValue as ObservableCollection<Pin>;
if (thisInstance == null ||
newPinsSource == null)
return;
UpdatePinsSource(thisInstance, newPinsSource);
newPinsSource.CollectionChanged += thisInstance.PinsSourceOnCollectionChanged;
}
private void PinsSourceOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
UpdatePinsSource(this, sender as IEnumerable<Pin>);
}
private static void UpdatePinsSource(Map bindableMap, IEnumerable<Pin> newSource)
{
bindableMap.Pins.Clear();
foreach (var pin in newSource)
bindableMap.Pins.Add(pin);
}
}
Notes:
I have omitted the using statements and namespace declaration for the sake of simplicity.
In order for our original Pins property to be updated as we add members to our bindable PinsSource property, I declared the PinsSource as ObservableCollection<Pin> and subscribed to its CollectionChanged event. Obviously, you can define it as an IList if you intend to only change the whole value of your bound property.
My final word regarding the 2 first answers to this question:
Although having a View control as a ViewModel property exempts us from writing business logic in code behind, but it still feels kind of hacky. In my opinion, the whole point of (well, at least a key point in) the VM part of the MVVM is that it is totally separate and decoupled from the V. Whereas the solution provided in the above-mentioned answers is actually this:
Insert a View Control into the heart of your ViewModel.
I think this way, not only you break the MVVM pattern but also you break its heart!
I have two options which worked for me and which could help you.
You could either add a static Xamarin.Forms.Maps Map property to your ViewModel and set this static property after setting the binding context, during the instantiation of your View, as show below:
public MapsPage()
{
InitializeComponent();
BindingContext = new MapViewModel();
MapViewModel.Map = MyMap;
}
This will permit you to access your Map in your ViewModel.
You could pass your Map from your view to the ViewModel during binding, for example:
<maps:Map
x:Name="MyMap"
IsShowingUser="true"
MapType="Hybrid" />
<StackLayout Orientation="Horizontal" HorizontalOptions="Center">
<Button x:Name="HybridButton" Command="{Binding MapToHybridViewChangeCommand}"
CommandParameter="{x:Reference MyMap}"
Text="Hybrid" HorizontalOptions="Center" VerticalOptions="Center" Margin="5"/>`
And get the Map behind from the ViewModel's Command.
Yes, Map.Pins is not bindable, but there is ItemsSource, which is easy to use instead.
<maps:Map ItemsSource="{Binding Locations}">
<maps:Map.ItemTemplate>
<DataTemplate>
<maps:Pin Position="{Binding Position}"
Label="{Binding Name}"
Address="{Binding Subtitle}" />
So, just for the pins, MVVM can be done without any custom control.
But Map.MoveToRegion() (and Map.VisibleRegion to read) is still open. There should be a way to bind them. Why not both in a single read/write property? (Answer: because of an endless loop.)
Note: if you need Map.MoveToRegion only once on start, the region can be set in the constructor.
I don't think Pins is a bindable property on Map, you may want to file feature request at Xamarin's Uservoice or the fourm here: http://forums.xamarin.com/discussion/31273/
It is not ideal, but you could listen for the property changed event in the code behind and then apply the change from there. Its a bit manual, but it is doable.
((ViewModels.YourViewModel)BindingContext).PropertyChanged += yourPropertyChanged;
And then define the "yourPropertyChanged" method
private void yourPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if(e.PropertyName == "YourPropertyName")
{
var position = new Position(37.79762, -122.40181);
Map.MoveToRegion(new MapSpan(position, 0.01, 0.01));
Map.Pins.Add(new Pin
{
Label = "Xamarin",
Position = position
});
}
}