setting up a simple component with data binding - xamarin.forms

I am trying to set up a component with data binding. This is basically a seperate content view that would have a property Item of type Item and supports binding. The following is the definition for the binding:
public static readonly BindableProperty ItemProperty
= BindableProperty.Create(
nameof(Item), typeof(Item), typeof(ItemComponent), null,
defaultBindingMode: BindingMode.TwoWay,
propertyChanged: ItemPropertyChanged);
private readonly ItemComponentViewModel vm;
static void ItemPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var view = (ItemComponent)bindable;
view.Item = (Item)newValue;
}
public Item Item
{
get => (Item)GetValue(ItemProperty);
set
{
SetValue(ItemProperty, value);
if (vm != null) vm.Data = value; // break point here
}
}
The item doesn't seem to get bound. The commented line had a breakpoint and doesn't break. The complete source code is here: https://github.com/neville-nazerane/xamarin-component-sample
The above code can be found in the ItemComponent class. This component is called in the MainPage class.
Update
Just to explain what I am trying to simulate and why:
Why do we use MVVM in pages? While we'll have better type safety and performance by using the behind code directly, when the page's logic gets bigger, it becomes cleaner to handle it with a view model and to have a view that is simply bound to it.
Why do we have components? So that we can reuse a UI we intend to use with some functionality. If this functionality becomes complex it might need a view model for the same reason explained above. Hence, if pages need view models, I don't see why components won't need them at some point too.
This being considered this does feel like a particle requirement without easy to find examples.

So after looking at your example it turns out it's a bit of a complicated problem. So if my explanation is not clear, please let me know.
Basically the problem lies in these 2 code pieces:
MainPage.xaml(line 14):
<local:ItemComponent Item="{Binding Demo}" />
ItemComponent.xaml.cs (line 43):
public ItemComponent()
{
InitializeComponent();
vm = new ItemComponentViewModel();
BindingContext = vm; //this breaks the functionality
}
The first part you tell it to bind to the Demo property, and as normal it looks for this property in it's BindingContext. However in the second part you override it's BindigContext and set it to a ItemComponentViewModel this ViewModel however does not have a property Demo so the {Binding Demo} does not work on this new BindingContext you've set.
Now a possible solution for your demo application would be to change MainPage.xaml to the following 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:local="clr-namespace:SampleApp"
x:Class="SampleApp.MainPage"
x:DataType="local:MainViewModel"
x:Name="MyDemoPage">
<StackLayout>
<Label Text="Manual:" />
<Label Text="{Binding Demo.Title}" />
<Label Text="Component: " />
<local:ItemComponent Item="{Binding Path=BindingContext.Demo, Source={x:Reference MyDemoPage}}" />
</StackLayout>
</ContentPage>
Basically we now place the Demo binding outside of the BindingContext of our ItemComponent control. However if you want to use it in a ListView (if I remember correctly from your original question, this solution might not work and it's possible you'll have to drop the ItemComponentViewModel and bind directly to the properties (ListView will already make sure that the BindingContext of your ItemComponent is set to the current Item, no need to pass it around through a bindable property.
Hope this helps!

Related

Xamarin.Forms Open ContentView with parameters in xaml

In my app there is a certain listview that I use over and over in my app, only with different elements in it. Therefore, I put everything inside a contentview and inflated it in my xaml like so:
<ContentPage Title="Newbies" BackgroundColor="#fafafa">
<views:CV_AllAdsRes />
</ContentPage>
The class looks like this:
public partial class CV_AllAdsRes : ContentView
{
public CV_AllAdsRes(int id)
{
InitializeComponent();
SetAds();
}
}
Now, this doenst work, because I am not using a "default constructor". If I remove the "int id" from the constructor, it works no problem. But I need to be able to inflate this content view with different parameters inside the xaml.
Am I understanding this concept wrong?
How can I inflate my content view and give it parameters via xaml?
Thank you
I solved it by using a second constructor next to the default one and giving it arguments from xaml like so:
<views:CV_AllAdsRes >
<x:Arguments >
<x:Int32>5</x:Int32>
</x:Arguments>
</views:CV_AllAdsRes>
this will give ID=5.

Xamarin Forms / ReactiveUI - Using ReactiveUI for masterdetail page shows that viewmodel is null

I have a xamarin forms application that is based on ReactiveUI. The viewmodels inherit from ReactiveObject and the codebehind the xaml of the pages , they inherit/are based on from ReactiveContentPage, in case of the masterdetailpage it inherits from the ReactiveMasterDetailPage. The contentpages/masterdetailpage it self are based on ReactiveContent.
What I want to achieve is the following: Get views/xamlpages by giving a viewmodel type. I have the following code for that. But it gives a null at _viewLocator.Resolve...
public async Task<TViewModel> PushViewModelAsync<TViewModel>(bool animated) where TViewModel : class
{
var viewModel = DependencyInjectionService.Get<TViewModel>();
var view = _viewLocator.ResolveView(viewModel);
if (view is Page page)
{
view.ViewModel = viewModel;
await Application.Current.MainPage.Navigation.PushAsync(page, animated);
return viewModel;
}
else
{
throw new ArgumentException($"resolved view for {typeof(TViewModel)} is not a page.");
}
}
The problem is that this works for normal contentpages but it doesnt work for my MasterDetailPage, how is that possible?
<?xml version="1.0" encoding="utf-8" ?>
<rxui:ReactiveMasterDetailPage 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"
mc:Ignorable="d"
xmlns:vm="clr-namespace:DriverApp.ViewModels"
x:TypeArguments="vm:MainViewModel"
xmlns:rxui="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms"
xmlns:local="clr-namespace:DriverApp.Views; assembly=MasterDetailPageNavigation"
x:Class="DriverApp.Views.MainPage"
Title="Personal Organiser">
<MasterDetailPage.Master>
<local:MasterPage x:Name="masterPage" />
</MasterDetailPage.Master>
<MasterDetailPage.Detail>
<NavigationPage>
<x:Arguments>
<local:PlanningPage/>
</x:Arguments>
</NavigationPage>
</MasterDetailPage.Detail>
The viewmodel:
public class MainViewModel : ReactiveObject, IActivatableViewModel
{
}
I inject it like this:
services.AddTransient<IViewFor<MainViewModel>, MainPage>();
var viewModel = DependencyInjectionService.Get<TViewModel>();
var view = _viewLocator.ResolveView(viewModel);
Based on the code provided I would expect the value you are passing into the _viewLocator.ResolveView to be null. You showed the code where you are registering the IViewFor but I don't see any code registering the ViewModel itself.
Registering IViewFor<Foo>, Foo doesn't register the view model. It tells the type system that a given ViewModel will resolve a specific page.
Also, it seems like you are using a different container than the one provided by ReactiveUI. Which is okay, but you have to make sure all your dependencies are registered correctly in the container you plan to resolve dependencies from.
Lastly. You say it gives a null, but you don't say if the ViewModel you are passing is null or the object you are using to resolve is null.
Either way, I think this is an issue of having the dependencies registered in the correct place, based on the information provided.

ReactiveTabbedPage Data Binding

I have been using ReactiveUI for a while with Xamarin Forms, but I've hit a brick wall when trying to use a ReactiveTabbedPage. I can't figure out how the ViewModel will get bound to the ReactiveContentPage's that are the children of the ReactiveTabbedPage.
So, as an example, I might have the following XAML:
<ReactiveTabbedPage x:Name="TabbedPage">
<local:Page1View x:Name="Page1" />
<local:Page2View x:Name="Page2" />
</ReactiveTabbedPage>
Where Page1View and Page2View are both of type ReactiveContentPage and T is the associated ViewModel.
What I expected to happen was that when the ReactiveTabbedPage was navigated to, Page1View would be displayed, and the ViewModel would be loaded (in the same way it would if I navigated to the Page1View directly). However, the ViewModel never gets called (the constructor is never fired and no data binding occurs).
However, both Page1View and Page2View do render and I can see the initial data that is created in those views (e.g. default text for labels etc.).
I know that the ViewModel stuff is working correctly, because if I navigate to Page1View directly (e.g. not in the ReactiveTabbedPage) everything displays as I expect.
Have I missed something, or am I going about this the wrong way? Or is this just not supported in the current version of RxUI?
Any advice is greatly appreciated!
The responsibility for tying the VM to the child pages lies with the host page (i.e. the ReactiveTabbedPage). It alone knows which VM corresponds to which view.
Let's take this one step at a time. First of all, the MainViewModel:
public class MainViewModel : ReactiveObject
{
public ChildViewModel1 Child1 => new ChildViewModel1();
public ChildViewModel2 Child2 => new ChildViewModel2();
}
This code obviously isn't realistic because you wouldn't want to recreate the child VMs upon every property access. It's more the API that's pertinent here.
ChildViewModel1 looks like this:
public class ChildViewModel1 : ReactiveObject
{
public string Test => "Hello";
}
And ChildViewModel2 looks much the same.
Now we can go about setting the views up. Our MainView.xaml looks like this:
<?xml version="1.0" encoding="utf-8" ?>
<rxui:ReactiveTabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:TypeArguments="vms:MainViewModel"
xmlns:local="clr-namespace:ReactiveTabbedPageTest"
xmlns:rxui="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms"
xmlns:vms="clr-namespace:ReactiveTabbedPageTest.VMs"
x:Class="ReactiveTabbedPageTest.MainView">
<local:Child1View x:Name="child1View" Title="Child 1"/>
<local:Child2View x:Name="child2View" Title="Child 2"/>
</rxui:ReactiveTabbedPage>
Notice it declares each of the child views. We need to hook up the VMs to those views, which we do in the code-behind for MainView:
public partial class MainView : ReactiveTabbedPage<VMs.MainViewModel>
{
public MainView()
{
InitializeComponent();
this.ViewModel = new VMs.MainViewModel();
this.WhenActivated(
disposables =>
{
this
.OneWayBind(this.ViewModel, x => x.Child1, x => x.child1View.ViewModel)
.DisposeWith(disposables);
this
.OneWayBind(this.ViewModel, x => x.Child2, x => x.child2View.ViewModel)
.DisposeWith(disposables);
});
}
}
I've done this the safest way by using WhenActivated and OneWayBind calls. In reality, it's unlikely your child VMs will change, so directly assigning them rather than binding is totally fine.
Now our child views can be thrown together. Here's ChildView1.xaml:
<?xml version="1.0" encoding="utf-8" ?>
<rxui:ReactiveContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ReactiveTabbedPageTest.Child1View"
x:TypeArguments="vms:ChildViewModel1"
xmlns:rxui="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms"
xmlns:vms="clr-namespace:ReactiveTabbedPageTest.VMs">
<Label x:Name="label" VerticalTextAlignment="Center" HorizontalTextAlignment="Center"/>
</rxui:ReactiveContentPage>
And the code-behind:
public partial class Child1View : ReactiveContentPage<ChildViewModel1>
{
public Child1View()
{
InitializeComponent();
this.WhenActivated(
disposables =>
{
this
.OneWayBind(this.ViewModel, x => x.Test, x => x.label.Text)
.DisposeWith(disposables);
});
}
}
Once again we're doing the usual RxUI binding goodness to associate properties in the VM with controls in the UI. And once again you could optimize this for properties that don't mutate.
For the purposes of this example, ChildView2 is much the same as ChildView1, but obviously it could be totally different.
The end result is as you'd expect:
What's not evident from the screenshot but is very important is that each tab is deactivating when you switch away from it (as would its associated view model if it implemented ISupportsActivation). This means you can clean up any bindings and subscriptions for that tab when it's not in use, reducing memory pressure and improving performance.

flex Property ignored

I'm trying to get an itemrenderer to display the text from a tweet, and (if there is one) the uploaded image. Text displays fine, but no matter what I do, the image doesn't show. Could it be that because I'm refering to a property that does not always exist, in this case "data.entities.media.creative.media_url" (it only exists if there is an image uploaded with the tweet) that the itemrenderer just ignores the property for all tweets?
Here's my code:
[Bindable(event="dataChange")]
public function get data():Object
{
return _data;
}
public function set data(value:Object):void
{
_data = value;
dispatchEvent(new Event("dataChange"));
}
]]>
</fx:Script>
<s:Label text="{data.text}"
width="100%"
styleName="tweetlist"/>
<s:Image id="tweetImage" source="{data.entities.media.creative.media_url}"/>
I am quite new to Flex, and so far I managed to get things working by pasting code together from different sources. So, any help would be greatly appreciated.
I see that you are binding to a deeply nested property: data.entities.media.creative.media_url. If you want the binding to work properly, you must make sure that each and every property on that path is made Bindable.
Concretely:
data must be Bindable on the ItemRenderer; it is by default when you extend ItemRenderer, so I would remove your custom Bindable declaration and event dispatching (or even remove the data getter/setter override altogether)
the entities property of the data instance must be declared Bindable
the media property of the entities instance must be declared Bindable
and so forth all the way to the media_url property
However it is not considered good coding practice (in general, not just in Flex) to access deeply nested properties like that, so I would advise you to encapsulate the property that you need so that you can bind to it like this:
<s:Image id="tweetImage" source="{data.media_url}" />
This will not only be better from a "clean code" point of view, but also for performance if you have a lot of items in the List. (Because the number of bindings will descrease by factor 5)
An alternative solution is to not use data binding and simply set tweetImage's source property:
override public function set data(value:Object):void {
super.data = value;
tweetImage.source = data.entities.media.creative.media_url;
}

Flex - Sending a parameter to a custom ItemRenderer?

What I am trying to accomplish to to get financial data in my Flex Datagrid to be color-coded--green if it's positive; red if it's negative. This would be fairly straightforward if the column I want colored was part of the dataProvider. Instead, I am calculating it based on two other columns that are part of the dataProvider. That would still be fairly straightforward because I could just calculate it again in the ItemRenderer, but another part of the calculation is based on the value of a textBox. So, what I think I need to be able to do is send the value of the textBox to the custom ItemRenderer, but since that value is stored in the main MXML Application, I don't know how to access it. Sending it as a parameter seems like the best way, but perhaps there's another.
Here is the current code for my ItemRenderer:
package {
import mx.controls.Label;
import mx.controls.listClasses.*;
public class PriceLabel extends Label {
private const POSITIVE_COLOR:uint = 0x458B00 // Green
private const NEGATIVE_COLOR:uint = 0xFF0000; // Red
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void {
super.updateDisplayList(unscaledWidth, unscaledHeight);
/* Set the font color based on the item price. */
setStyle("color", (data.AvailableFunding >= 0) ? NEGATIVE_COLOR : POSITIVE_COLOR);
}
}
(data.AvailableFunding doesn't exist)
So does anyone know how I would go about accomplishing this?
You may want to look into ClassFactory from the Flex APIs:
This allows you to set a prototype Object with arbitrary types / values each of which will be passed to the item renderer. From the sample:
var productRenderer:ClassFactory = new ClassFactory(ProductRenderer);
productRenderer.properties = { showProductImage: true };
myList.itemRenderer = productRenderer;
The above code assumed that "ProductRenderer" has a public property called "showProductImage" which will be set with a value of "true."
Ah, so I knew about outerDocument but not parentDocument. I was able to just use parentDocument.*whatever I want from the main App and I can access it as long as it's public.
Example:
setStyle("color", (parentDocument.availableFunding >= 0) ? POSITIVE_COLOR : NEGATIVE_COLOR);
Sweet! :)
You can access the value of the TextBox directly, if you need to, by using the static Application.application object, which is accessible from anywhere in your application.
For example, if you wanted the renderers to be notified when the value of the TextInput control changes, you could do something like this (from within your ItemRenderer, and where myTextInput is the ID of the control defined in your main MXML class):
<mx:Script>
<![CDATA[
import mx.core.Application;
private function creationCompleteHandler(event:Event):void
{
Application.application.myTextInput.addEventListener(TextEvent.TEXT_INPUT, handleTextInput, false, 0, true);
}
private function handleTextInput(event:TextEvent):void
{
if (event.currentTarget.text == "some special value")
{
// Take some action...
}
}
]]>
</mx:Script>
With this approach, each item-renderer object will be notified when the TextInput's text property changes, and you can take appropriate action based on the value of the control at that time. Notice as well that I've set the useWeakReference argument to true in this case, to make sure the listener assignments don't interfere unintentionally with garbage collection. Hope it helps!
There's another technique, which, while it initially feels a little hacky is perhaps less cumbersome and cleaner in actual use.
It involves the little-observed fact that an event dispatch is, of course, synchronous and the event object can be treated as a value object populated by any event handler.
i.e. the ItemRenderer can do something like:
...
var questionEvt:DynamicEvent = new DynamicEvent('answerMeThis', true, true);
if (dispatchEvent(questionEvt))
{
if (questionEvent.answer == "some value")
....
With a corresponding handler somewhere up the view hierarchy above the renderer that has a listener on the event and does something like:
function handleAnswerMeThis(event:DynamicEvent):void
{
event.answer = "another value";
event.dataHelper = new DataHelperThingy();
}
etc.
It need not be a DynamicEvent - I'm just using that for lazy illustrative purposes.
I vote up for cliff.meyers' answer.
Here's another example on setting the properties of an itemRenderer from MXML by building a function that wraps a ClassFactory around the itemRenderer class and that injects the necessary properties.
The static function:
public static function createRendererWithProperties(renderer:Class,
properties:Object ):IFactory {
var factory:ClassFactory = new ClassFactory(renderer);
factory.properties = properties;
return factory;
}
A simple example that adds a Tooltip to each item in a list:
<mx:List dataProvider="{['Foo', 'Bar']}" itemRenderer="{createRendererWithProperties(Label, {toolTip: 'Hello'})}"/>
Reference:
http://cookbooks.adobe.com/post_Setting_the_properties_of_an_itemRenderer_from_MXM-5762.html
You use outerDocument property. Please see the fx:Component reference.
You could create an 'AvailableFunding' static variable in the ItemRenderer and then set it in the parent document.
public class PriceLabel extends Label {
public static var availableFunding:int;
...
...
SetStyle("color", (PriceLabel.availableFunding >= 0) ? NEGATIVE_COLOR : POSITIVE_COLOR);
}
In your parent document, set it when your text box gets updated
PriceLabel.availableFunding = textBox.text;
Obviously it'll be the same value for every ItemRenderer but it looks like that might be what you're doing anyway.
I like to override the set data function of the item renderer to change the renderer when the data provider changes as shown here
When you override the function you could cast the object to your object to make the availableFunding property available.
To access the text box you could try creating a public property and binding the property to the text box in the mxml file:
public var textVar:String;
<mx:itemRenderer>
<mx:Component>
<customrenderer textVar="{txtBox.text}" />
</mx:Component>
</mx:itemRenderer>
Nice ClassFactory Example here
See this example:
itemRenderer="{UIUtils.createRenderer(TextBox,{iconSrc:IconRepository.linechart,headerColor:0xB7D034,subHeaderColor:0xE3007F,textColor:0x75757D})}"

Resources