I'm trying to do a hello world with Reactive UI and Xamarin Forms.
I have created a ViewModel (based on ReactiveObject), a custom page (based on ReactiveContentPage) and some XAML markup.
The page has an entry and a Label, bound together. When I run it (on iOS and Android), it appears to work for the first few characters typed, then locks up. The console gives a 'too much is happening on the main thread' warning.
What am I missing?
The project is here.
The Model
namespace XamarinFormsReactiveUIExample
{
public class MyPageModel : ReactiveObject
{
private string _userName = "";
public string UserName {
get { return _userName; }
set { this.RaiseAndSetIfChanged (ref _userName, value); }
}
}
}
The Page
namespace XamarinFormsReactiveUIExample
{
public partial class MyPage : ReactiveContentPage<MyPageModel>
{
public MyPage ()
{
InitializeComponent ();
}
}
}
The XAML
<?xml version="1.0" encoding="UTF-8"?>
<reactive:ReactiveContentPage
x:TypeArguments="local:MyPageModel"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XamarinFormsReactiveUIExample.MyPage"
xmlns:local="clr-namespace:XamarinFormsReactiveUIExample;assembly=XamarinFormsReactiveUIExample"
xmlns:reactive="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms">
<reactive:ReactiveContentPage.BindingContext>
<local:MyPageModel />
</reactive:ReactiveContentPage.BindingContext>
<reactive:ReactiveContentPage.Padding>
<OnPlatform x:TypeArguments="Thickness"
iOS="0, 150, 0, 0" />
</reactive:ReactiveContentPage.Padding>
<reactive:ReactiveContentPage.Content>
<StackLayout>
<Entry Text = "{Binding UserName}"></Entry>
<Label Text = "{Binding UserName}"></Label>
</StackLayout>
</reactive:ReactiveContentPage.Content>
</reactive:ReactiveContentPage>
I don't see how this could be anything but a Xamarin bug, ReactiveUI isn't doing anything here. Anyways, you can always try to write these bindings the RxUI way:
<Entry x:Name="userNameEntry" />
<Label x:Name="userNameLabel" />
Then, in the constructor:
public MyPage ()
{
InitializeComponent();
this.ViewModel = new MyPageModel();
this.Bind(ViewModel, vm => vm.UserName, v => v.userNameEntry.Text);
this.OneWayBind(ViewModel, vm => vm.UserName, v => v.userNameLabel.Text);
}
The only thing I saw missing was the ViewModel was never instantiated, and the Binding Context was never set. I can't see what your old code did before you changed it to the new style. Personally, I try to avoid using code behind as much as possible as well. So you should be able to modify your constructor to something like,
public MyPage ()
{
InitializeComponent();
this.ViewModel = new MyPageModel();
this.BindingContext = ViewModel;
}
And revert to using your XAML binding.
Related
I have a SearchResult class that binds to a ListView. What I want to do specifically is highlight the snippet inside the search result text that matches the query the user entered.
The relevant XAML looks something like this (omitting the fluff):
<DataTemplate>
<StackPanel>
<!-- Search result -->
<RichTextBlock>
<!-- Would this idea work? -->
<RichTextBlock.TextHighlighters>
<TextHighlighter>
<TextHighlighter.Ranges>
<!-- Add the bound range here-->
<!-- {Binding Range} or text highlighter or something -->
</TextHighlighter.Ranges>
</TextHighlighter>
</RichTextBlock.TextHighlighters>
<Paragraph>
<Run Text="{Binding Text}"></Run>
</Paragraph>
</RichTextBlock>
</StackPanel>
</DataTemplate>
I can add whatever property from the SearchResult class, be it a TextHighlighter or a TextRange. I just don't know whether the XAML syntax allows plugging in that value.
I've also thought of doing this in code, but I do want to keep the search item template inside the XAML, and not put it in C#. However, it would be possible to do something like lvSearchResults.Items[i]... or whatever it takes to put in the highlighter or range. I just can't figure out the correct method at the moment.
If you are planning to create a locally highlighted search result list, you can try this way:
Create a search result class
public class SearchResult
{
public string DisplayText { get; set; }
public string HighlightText { get; set; }
}
Create a UserControl to show the result
SearchResultBlock.xaml
<Grid>
<TextBlock x:Name="ResultBlock" TextWrapping="Wrap" MaxLines="2"
TextTrimming="CharacterEllipsis"/>
</Grid>
SearchResultBlock.xaml.cs
public sealed partial class SearchResultBlock : UserControl
{
public SearchResultBlock()
{
this.InitializeComponent();
}
public SearchResult Result
{
get { return (SearchResult)GetValue(ResultProperty); }
set { SetValue(ResultProperty, value); }
}
public static readonly DependencyProperty ResultProperty =
DependencyProperty.Register("Result", typeof(SearchResult), typeof(SearchResultBlock), new PropertyMetadata(null,new PropertyChangedCallback(Result_Changed
private static void Result_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if(e.NewValue!=null && e.NewValue is SearchResult data)
{
var instance = d as SearchResultBlock;
instance.ResultBlock.Inlines.Clear();
var sp = data.DisplayText.Split(data.HighlightText);
instance.ResultBlock.Inlines.Add(new Run { Text = sp.First() });
instance.ResultBlock.Inlines.Add(new Run { Text = data.HighlightText, Foreground = new SolidColorBrush(Colors.Red) });
if (sp.Length > 1)
instance.ResultBlock.Inlines.Add(new Run { Text = sp.Last() });
}
}
}
Use it in DataTemplate
<DataTemplate x:DataType="SearchResult" x:Key="ResultItemTemplate">
<SearchResultBlock Result="{Binding}"/>
</DataTemplate>
By string splitting, create different types of Runs and merge them in the TextBlock. This can also achieve the highlighting effect.
Best regards.
I am working on Xamarin.Forms. I have login page for that I am using StackLayout with ScrollView.
Code following
<?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="simpleListView.Pages.LoginPage">
<ContentPage.Content>
<ScrollView Orientation="Vertical" x:Name="scroll">
<StackLayout Padding="20">
<Label Margin="20" HorizontalOptions="Center" Text="Login below" ></Label>
<BoxView HeightRequest="150" Color="Accent"></BoxView>
<Entry Text="{Binding Email}" x:Name="txtEmail" Placeholder="Email"></Entry>
<Entry Text="{Binding Password}" x:Name="txtPass" Placeholder="Paasword"></Entry>
<Button x:Name="btnLogin" BackgroundColor="Blue" Text="Login" Command="{Binding SubmitCommand}"></Button>
</StackLayout>
</ScrollView>
</ContentPage.Content>
</ContentPage>
But the code above not scrolling layout towards top when keypad open & hiding almost half of Login button. How can I implement scrolling when keypad open? I am not much interested in using custom renderers.
See login page screeshot
Since there is (as far as I am informed) no good way to get the actual keyboard height for android via a DependencyService, using that scrollview is the right approach to begin with, however what you want to do is UI customizing which leads to custom renderers. Saying "i want a cool custom UI but I am not interested in custom renderers" is like "I want to drive, but I am not interested in wheels".
However, to make it a bit easier for you, I happen to have code for a custom renderer, which gives the desired result:
Android
[assembly: ExportRenderer(typeof(ModernLogin), typeof(ModernLoginPageRenderer))]
namespace MyApp.Droid.Renderer
{
public class ModernLoginPageRenderer : PageRenderer
{
public ModernLoginPageRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
base.OnElementChanged(e);
// Set SoftInput.AdjustResize for this window
if (e.NewElement != null)
{
(this.Context as FormsApplicationActivity).Window.SetSoftInputMode(SoftInput.AdjustResize);
}
}
protected override void OnWindowVisibilityChanged([GeneratedEnum] ViewStates visibility)
{
// Revert to default SoftInputMode after moving away from this window
if (visibility == ViewStates.Gone)
{
(this.Context as FormsApplicationActivity).Window.SetSoftInputMode(SoftInput.AdjustPan);
}
base.OnWindowVisibilityChanged(visibility);
}
protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
{
base.OnLayout(changed, left, top, right, bottom);
}
}
}
iOS
[assembly: ExportRenderer(typeof(ModernLogin), typeof(ModernLoginPageRenderer))]
namespace MyApp.iOS.Renderer
{
public class ModernLoginPageRenderer : PageRenderer
{
NSObject observerHideKeyboard;
NSObject observerShowKeyboard;
public override void ViewDidLoad()
{
base.ViewDidLoad();
var cp = Element as ModernLogin;
if (cp != null)
{
foreach (var g in View.GestureRecognizers)
{
g.CancelsTouchesInView = true;
}
}
}
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
observerHideKeyboard = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillHideNotification, OnKeyboardNotification);
observerShowKeyboard = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillShowNotification, OnKeyboardNotification);
}
public override void ViewWillDisappear(bool animated)
{
base.ViewWillDisappear(animated);
NSNotificationCenter.DefaultCenter.RemoveObserver(observerHideKeyboard);
NSNotificationCenter.DefaultCenter.RemoveObserver(observerShowKeyboard);
}
void OnKeyboardNotification(NSNotification notification)
{
if (!IsViewLoaded) return;
var frameBegin = UIKeyboard.FrameBeginFromNotification(notification);
var frameEnd = UIKeyboard.FrameEndFromNotification(notification);
var page = Element as ModernLogin;
if (page != null)
{
var padding = page.Padding;
page.Padding = new Thickness(padding.Left, padding.Top, padding.Right, padding.Bottom + frameBegin.Top - frameEnd.Top);
}
}
}
}
Make sure you replace the name of "ModernLogin" with the name of your login page class. If your LoginPage is a regular ContentPage, I would recommend creating a new class inheriting from ContentPage.
I have a view model that inherits from ReactiveObject from reactiveui.net, something like
public sealed class TestViewModel : ReactiveObject
{
public sealed class NestedViewModel
{
private string _property;
public string VMProperty
{
get { return _property; }
set { this.RaiseAndSetIfChanged(ref _property, value); }
}
private string _suffix;
public string Suffic
{
get { return _suffix; }
set { this.RaiseAndSetIfChanged(ref _suffix, value); }
}
}
private NestedViewModel _nested = new NestedViewModel();
public Nested
{
get { return _nested; }ยจ
set { this.RaiseAndSetIfChanged(ref _nested, value); }
}
#if DEBUG
public TestViewModel() {
Nested.VMProperty = "Test string";
Nested.Suffix = "with suffix";
}
#endif
}
I can get the following to display both design-time and run-time:
<Page.DataContext>
<local:TestViewModel />
</Page.DataContext>
<TextBlock Text="{Binding Nested.VMProperty}" />
<TextBlock Text="{Binding Nested.Suffix}" />
but when I try to do this instead, no text is displayed design-time:
<Page.DataContext><!-- ... -->
<TextBlock>
<Run Text="{Binding Nested.VMProperty}" />
<Run Text="{Binding Nested.Suffix}" />
</TextBlock>
Run-time it still works, but I don't want to have to deploy to the device emulator every time I want to check some pixel pushing...
How do I get these properties do display inside a <Run /> tag during design time?
Paul Betts, the creator of the ReactiveUI framework, advocates hard-coding sample data into the Page's XAML:
<TextBlock>
<Run Text="Test String" x:Name="VMproperty" />
<Run Text="with suffix" x:Name="Suffix" />
</TextBlock>
You can then do the binding in the Page's code behind:
this.OneWayBind(ViewModel, vm => vm.VMproperty, v => v.VMproperty.Text);
this.OneWayBind(ViewModel, vm => vm.Suffix, v => v.Suffix.Text);
These ReactiveUI style bindings overwrite the sample data that was hard coded in the XAML. So you get sample data at design-time and data binding at runtime.
Source: https://groups.google.com/d/msg/reactivexaml/GrVHWm8tUuM/4EAxOsxc_LQJ
What about using FallbackValue in your binding? I use it frequently to check how bound content would look like and didn't have any problem.
I'm developing a Windows 8.1 Universal App using the MVVM Light Toolkit.
I have a ListView
<ListView
x:Name="MyListView"
ItemsSource="{Binding ModelCollection, Mode=TwoWay}"
SelectedItem="{Binding CurrentModelItem, Mode=TwoWay}"
and my ViewModel is as it follows:
public class PageViewModel : ViewModelBase
{
private ObservableCollection<MyViewModel> _modelCollection =
new ObservableCollection<MyViewModel>();
public ObservableCollection<MyViewModel> ModelCollection
{
get { return _modelCollection ; }
set { Set(() => ModelCollection , ref _modelCollection , value); }
}
private MyViewModel _currentModelItem;
public InspectionDiscrepancyViewModel CurrentModelItem
{
get { return _currentModelItem; }
set { Set(() => CurrentModelItem, ref _currentModelItem, value); }
}
If I let the CurrentModelItem item at load time it works 'fine' but if I try to set the CurrentModelItem in the LoadData Method -that populates the ObservableCollection- to the first ModelCollection Item
CurrentModel = ModelCollection.FirstOrDefault();
I get the following error on the Set() Method of the MVVM Light Toolkit:
An exception of type 'System.ArgumentException' occurred in mscorlib.dll but was not handled in user code
Additional information: Value does not fall within the expected range.
I've seen that doing this should be very straightforward, what am I doing wrong here?
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
});
}
}