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"
xmlns:local="clr-namespace:mvvmtest"
x:Class="mvvmtest.mvvmtestPage"
BindingContext="{Binding TestViewModel, Source={StaticResource Locator}}">
<Label
Text="Welcome to Xamarin Forms!"
VerticalOptions="Center"
HorizontalOptions="Center" />
</ContentPage>
App:
public App()
{
InitializeComponent();
var locator = new ViewLocator();
Current.Resources.Add("Locator", locator);
MainPage = new mvvmtestPage();
}
ViewLocator:
public class ViewLocator
{
public TestViewModel TestViewModel
{
get { return new TestViewModel(); }
}
}
TestViewModel:
public class TestViewModel
{
public static int InstancesNum = 0;
public TestViewModel()
{
Debug.WriteLine(++InstancesNum);
}
}
Wondering why the TesViewModel constructor is called twice.
Call Stack:
mvvmtest.ViewLocator.get_TestViewModel() in /Users/xxx/Projects/mvvmtest/mvvmtest/ViewLocator.cs:8
System.Reflection.MonoMethod.InternalInvoke() in
Xamarin.Forms.BindingExpression.BindingExpressionPart.TryGetValue(mvvmtest.ViewLocator source, mvvmtest.ViewLocator value) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Core\BindingExpression.cs:565
Xamarin.Forms.BindingExpression.ApplyCore(mvvmtest.ViewLocator sourceObject, mvvmtest.mvvmtestPage target, Xamarin.Forms.BindableProperty property, bool fromTarget) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Core\BindingExpression.cs:160
Xamarin.Forms.BindingExpression.Apply(mvvmtest.ViewLocator sourceObject, mvvmtest.mvvmtestPage target, Xamarin.Forms.BindableProperty property) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Core\BindingExpression.cs:76
Xamarin.Forms.Binding.Apply(object newContext, mvvmtest.mvvmtestPage bindObj, Xamarin.Forms.BindableProperty targetProperty) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Core\Binding.cs:122
Xamarin.Forms.BindableObject.SetBinding(Xamarin.Forms.BindableProperty targetProperty, Xamarin.Forms.Binding binding, bool fromStyle) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Core\BindableObject.cs:281
Xamarin.Forms.BindableObject.SetBinding(Xamarin.Forms.BindableProperty targetProperty, Xamarin.Forms.Binding binding) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Core\BindableObject.cs:78
Xamarin.Forms.Xaml.ApplyPropertiesVisitor.TrySetBinding(mvvmtest.mvvmtestPage element, Xamarin.Forms.BindableProperty property, string localName, Xamarin.Forms.Binding value, Xamarin.Forms.Xaml.ElementNode lineInfo, System.Exception exception) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Xaml\ApplyPropertiesVisitor.cs:371
Xamarin.Forms.Xaml.ApplyPropertiesVisitor.SetPropertyValue(mvvmtest.mvvmtestPage xamlelement, Xamarin.Forms.Xaml.XmlName propertyName, Xamarin.Forms.Binding value, mvvmtest.mvvmtestPage rootElement, Xamarin.Forms.Xaml.ElementNode node, Xamarin.Forms.Xaml.HydratationContext context, Xamarin.Forms.Xaml.ElementNode lineInfo) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Xaml\ApplyPropertiesVisitor.cs:290
Xamarin.Forms.Xaml.ApplyPropertiesVisitor.Visit(Xamarin.Forms.Xaml.ElementNode node, Xamarin.Forms.Xaml.XamlLoader.RuntimeRootNode parentNode) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Xaml\ApplyPropertiesVisitor.cs:119
Xamarin.Forms.Xaml.ElementNode.Accept(Xamarin.Forms.Xaml.ApplyPropertiesVisitor visitor, Xamarin.Forms.Xaml.XamlLoader.RuntimeRootNode parentNode) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Xaml\XamlNode.cs:176
Xamarin.Forms.Xaml.RootNode.Accept(Xamarin.Forms.Xaml.ApplyPropertiesVisitor visitor, Xamarin.Forms.Xaml.INode parentNode) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Xaml\XamlNode.cs:224
Xamarin.Forms.Xaml.XamlLoader.Visit(Xamarin.Forms.Xaml.XamlLoader.RuntimeRootNode rootnode, Xamarin.Forms.Xaml.HydratationContext visitorContext) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Xaml\XamlLoader.cs:125
Xamarin.Forms.Xaml.XamlLoader.Load(mvvmtest.mvvmtestPage view, string xaml) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Xaml\XamlLoader.cs:76
Xamarin.Forms.Xaml.XamlLoader.Load(mvvmtest.mvvmtestPage view, System.RuntimeType callingType) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Xaml\XamlLoader.cs:56
Xamarin.Forms.Xaml.Extensions.LoadFromXaml<mvvmtest.mvvmtestPage>(mvvmtest.mvvmtestPage view, System.RuntimeType callingType) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Xaml\ViewExtensions.cs:36
mvvmtest.mvvmtestPage.InitializeComponent() in /Users/xxx/Projects/mvvmtest/mvvmtest/obj/Debug/mvvmtest.mvvmtestPage.xaml.g.cs:21
mvvmtest.mvvmtestPage..ctor() in /Users/xxx/Projects/mvvmtest/mvvmtest/mvvmtestPage.xaml.cs:10
mvvmtest.App..ctor() in /Users/xxx/Projects/mvvmtest/mvvmtest/App.xaml.cs:14
mvvmtest.iOS.AppDelegate.FinishedLaunching(UIKit.UIApplication app, Foundation.NSDictionary options) in /Users/xxx/Projects/mvvmtest/iOS/AppDelegate.cs:17
UIKit.UIApplication.UIApplicationMain() in
UIKit.UIApplication.Main(string[] args, System.IntPtr principal, System.IntPtr delegate) in /Users/builder/data/lanes/3985/62816dd6/source/xamarin-macios/src/UIKit/UIApplication.cs:79
UIKit.UIApplication.Main(string[] args, string principalClassName, string delegateClassName) in /Users/builder/data/lanes/3985/62816dd6/source/xamarin-macios/src/UIKit/UIApplication.cs:63
mvvmtest.iOS.Application.Main(string[] args) in /Users/xxx/Projects/mvvmtest/iOS/Main.cs:17
mvvmtest.ViewLocator.get_TestViewModel() in /Users/xxx/Projects/mvvmtest/mvvmtest/ViewLocator.cs:8
System.Reflection.MonoMethod.InternalInvoke() in
Xamarin.Forms.BindingExpression.BindingExpressionPart.TryGetValue(mvvmtest.ViewLocator source, mvvmtest.ViewLocator value) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Core\BindingExpression.cs:565
Xamarin.Forms.BindingExpression.ApplyCore(mvvmtest.ViewLocator sourceObject, mvvmtest.mvvmtestPage target, Xamarin.Forms.BindableProperty property, bool fromTarget) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Core\BindingExpression.cs:160
Xamarin.Forms.BindingExpression.Apply(mvvmtest.ViewLocator sourceObject, mvvmtest.mvvmtestPage target, Xamarin.Forms.BindableProperty property) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Core\BindingExpression.cs:76
Xamarin.Forms.Binding.Apply(mvvmtest.TestViewModel newContext, mvvmtest.mvvmtestPage bindObj, Xamarin.Forms.BindableProperty targetProperty) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Core\Binding.cs:122
Xamarin.Forms.BindableObject.ApplyBindings(mvvmtest.TestViewModel oldContext, bool skipBindingContext) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Core\BindableObject.cs:408
Xamarin.Forms.BindableObject.ApplyBindings(mvvmtest.TestViewModel oldContext) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Core\BindableObject.cs:123
Xamarin.Forms.BindableObject.SetInheritedBindingContext(mvvmtest.mvvmtestPage bindable, object value) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Core\BindableObject.cs:117
Xamarin.Forms.Element.SetChildInheritedBindingContext(mvvmtest.mvvmtestPage child, object context) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Core\Element.cs:482
Xamarin.Forms.Element.set_Parent(mvvmtest.App value) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Core\Element.cs:196
Xamarin.Forms.Application.set_MainPage(mvvmtest.mvvmtestPage value) in C:\BuildAgent3\work\ca3766cfc22354a1\Xamarin.Forms.Core\Application.cs:86
mvvmtest.App..ctor() in /Users/xxx/Projects/mvvmtest/mvvmtest/App.xaml.cs:14
mvvmtest.iOS.AppDelegate.FinishedLaunching(UIKit.UIApplication app, Foundation.NSDictionary options) in /Users/xxx/Projects/mvvmtest/iOS/AppDelegate.cs:17
UIKit.UIApplication.UIApplicationMain() in
UIKit.UIApplication.Main(string[] args, System.IntPtr principal, System.IntPtr delegate) in /Users/builder/data/lanes/3985/62816dd6/source/xamarin-macios/src/UIKit/UIApplication.cs:79
UIKit.UIApplication.Main(string[] args, string principalClassName, string delegateClassName) in /Users/builder/data/lanes/3985/62816dd6/source/xamarin-macios/src/UIKit/UIApplication.cs:63
mvvmtest.iOS.Application.Main(string[] args) in /Users/xxx/Projects/mvvmtest/iOS/Main.cs:17
Found a reported bugs: https://bugzilla.xamarin.com/show_bug.cgi?id=27299
https://bugzilla.xamarin.com/show_bug.cgi?id=45557
Most probably it's still not fixed.
Workaround:
<ContentPage.BindingContext>
<local:TestViewModel />
</ContentPage.BindingContext>
Related
Is it possible to create a generic converter between enum values and ints?
I have a set of similar converters in my project:
Object^ SpecificEnumIntConverter::Convert(Object^ value, TypeName targetType, Object^ parameter, String^ language)
{
const auto asEnum = static_cast<SpecificEnumType>(value);
const auto asInt = static_cast<int>(asEnum);
return asInt;
}
Object^ SpecificEnumIntConverter::ConvertBack(Object^ value, TypeName targetType, Object^ parameter, String^ language)
{
const auto asInt = static_cast<int>(value);
const auto asEnum = static_cast<SpecificEnumType>(asInt);
return asEnum;
}
These converters are usually used to binding with ViewModel, i.e:
<ListBox SelectedIndex="{x:Bind VM.EnumTypeProperty, Mode=TwoWay, Converter={StaticResource SpecificEnumIntConverter}}">
I am wondering if it is possible to implement a generic converter.
Thanks in advance and best regards!
It is possible to define generic converter by using template<...> somehow.
public enum class EnumType1 : int
{
Foo1, Bar1
};
public enum class EnumType2 : int
{
Foo2, Bar2
};
// C++/CLI's syntax "generic<...> public ref class ... " cannot be used in C++/CX.
template <class _T>
ref class GenericEnumConverter : Windows::UI::Xaml::Data::IValueConverter
{
public:
virtual Platform::Object ^ Convert(Platform::Object ^value, Windows::UI::Xaml::Interop::TypeName targetType, Platform::Object ^parameter, Platform::String ^language)
{
const auto asEnum = static_cast<_T>(value);
const auto asInt = static_cast<int>(asEnum);
return asInt;
}
virtual Platform::Object ^ ConvertBack(Platform::Object ^value, Windows::UI::Xaml::Interop::TypeName targetType, Platform::Object ^parameter, Platform::String ^language)
{
const auto asInt = static_cast<int>(value);
const auto asEnum = static_cast<_T>(asInt);
return asEnum;
}
};
However it's not possible to lay such converters directly in Resources via XAML (because templated-class in C++/CX cannot take "public" accessibility-modifier). So you need to deploy them in code behind.
[Windows::Foundation::Metadata::WebHostHidden]
public ref class MainPage sealed
{
public:
MainPage()
{
Resources->Insert(L"EnumType1Converter", ref new GenericEnumConverter<EnumType1>());
Resources->Insert(L"EnumType2Converter", ref new GenericEnumConverter<EnumType2>());
InitializeComponent();
}
property EnumType1 Value1 {
EnumType1 get() { return m_Value1; }
void set(EnumType1 value) {
m_Value1 = value;
//OutputDebugString((m_Value1.ToString() + L"\r")->Begin());
}
}
property EnumType2 Value2 {
EnumType2 get() { return m_Value2; }
void set(EnumType2 value) {
m_Value2 = value;
//OutputDebugString((m_Value2.ToString() + L"\r")->Begin());
}
}
private:
EnumType1 m_Value1 = EnumType1::Foo1;
EnumType2 m_Value2 = EnumType2::Bar2;
};
Then you can use the converters in binding.
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel>
<ListBox SelectedIndex="{x:Bind Value1, Converter={StaticResource EnumType1Converter}, Mode=TwoWay}" Margin="16">
<ListBoxItem>Foo1</ListBoxItem>
<ListBoxItem>Bar1</ListBoxItem>
</ListBox>
<ListBox SelectedIndex="{x:Bind Value2, Converter={StaticResource EnumType2Converter}, Mode=TwoWay}" Margin="16">
<ListBoxItem>Foo2</ListBoxItem>
<ListBoxItem>Bar2</ListBoxItem>
</ListBox>
</StackPanel>
</Grid>
There is a workaround below for you to try:
Use a variable typeFullName to save the enum type for later use in ConvertBack method to determine the type of the target enum class.
public ref class SpecificEnumIntConverter sealed : Windows::UI::Xaml::Data::IValueConverter
{
private:
Platform::String^ typeFullName;
public:
virtual Platform::Object^ Convert(Platform::Object^ value, Windows::UI::Xaml::Interop::TypeName targetType,
Platform::Object^ parameter, Platform::String^ language)
{
typeFullName = value->GetType()->FullName;
EnumType1 enum1 = EnumType1::red;
EnumType2 enum2 = EnumType2::red;
if (typeFullName == enum1.GetType()->FullName)
{
const auto asEnum = static_cast<EnumType1>(value);
const auto asInt = static_cast<int>(asEnum);
return asInt;
}
else if(typeFullName == enum2.GetType()->FullName)
{
const auto asEnum = static_cast<EnumType2>(value);
const auto asInt = static_cast<int>(asEnum);
return asInt;
}
}
// No need to implement converting back on a one-way binding
virtual Platform::Object^ ConvertBack(Platform::Object^ value, Windows::UI::Xaml::Interop::TypeName targetType,
Platform::Object^ parameter, Platform::String^ language)
{
if (typeFullName != nullptr)
{
EnumType1 enum1 = EnumType1::red;
EnumType2 enum2 = EnumType2::red;
if (typeFullName == enum1.GetType()->FullName)
{
const auto asInt = static_cast<int>(value);
const auto asEnum = static_cast<EnumType1>(asInt);
return asEnum;
}
else if (typeFullName == enum2.GetType()->FullName)
{
const auto asInt = static_cast<int>(value);
const auto asEnum = static_cast<EnumType2>(asInt);
return asEnum;
}
}
}
};
EnumType1 and EnumType2 are the sample enum types, red is the member within the enum class.
I have defined my style as such:
<ContentView.Resources>
<ResourceDictionary>
<Style TargetType="Entry" x:Key="IntegralEntryBehavior">
<Setter Property="Behaviors" Value="valid:EntryIntegerValidationBehavior"/>
</Style>
</ResourceDictionary>
</ContentView.Resources>
And multiple similar Entries:
<StackLayout Grid.Column="0" Grid.Row="0">
<Entry Style="{StaticResource IntegralEntryBehavior}"/>
</StackLayout>
If I define Entry behavior like this, I get an error, that Entry.Behaviors property is readonly, but it's possible to define behavior without using Style attribute inside Entry as such:
<Entry.Behaviors>
<valid:EntryIntegerValidationBehavior/>
</Entry.Behaviors>
What is the difference between these approaches and why does only the second one work? Is it possible to modify the first approach to make it work? I'm looking for a shorter way to define this behavior for each entry than the second option.
You can checkout the example here:
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/behaviors/creating#consuming-a-xamarinforms-behavior-with-a-style
Basically, add an attached property to your behavior and then set the style setter's property to that attached property. The attached property handles adding itself to the Entry that you attach it to.
public class EntryIntegerValidationBehavior : Behavior<Entry>
{
public static readonly BindableProperty AttachBehaviorProperty =
BindableProperty.CreateAttached ("AttachBehavior", typeof(bool), typeof(EntryIntegerValidationBehavior), false, propertyChanged: OnAttachBehaviorChanged);
public static bool GetAttachBehavior (BindableObject view)
{
return (bool)view.GetValue (AttachBehaviorProperty);
}
public static void SetAttachBehavior (BindableObject view, bool value)
{
view.SetValue (AttachBehaviorProperty, value);
}
static void OnAttachBehaviorChanged (BindableObject view, object oldValue, object newValue)
{
var entry = view as Entry;
if (entry == null) {
return;
}
bool attachBehavior = (bool)newValue;
if (attachBehavior) {
entry.Behaviors.Add (new EntryIntegerValidationBehavior ());
} else {
var toRemove = entry.Behaviors.FirstOrDefault (b => b is EntryIntegerValidationBehavior);
if (toRemove != null) {
entry.Behaviors.Remove (toRemove);
}
}
}
// Actual behavior code here
}
Finally edit your style to look like this:
<Style TargetType="Entry" x:Key="IntegralEntryBehavior">
<Setter Property="valid:EntryIntegerValidationBehavior.AttachBehavior" Value="true"/>
</Style>
I am using the sf-list-selector in my designer as shown below. I see my products list and I can select and sort.
<sf-list-selector sf-dynamic-items-selector sf-provider="properties.ProductProviderName.PropertyValue" sf-item-type="properties.ProductType.PropertyValue" sf-multiselect="true" sf-sortable="true" sf-master="true" sf-selected-ids="properties.ProductIds.PropertyValue" />
However I am getting an exception in the log file when I press save in the designer:
Requested URL :
https://localhost/Sitefinity/Services/Pages/ControlPropertyService.svc/batch/fc82280c-3055-6fae-9336-ff0000e88380/?pageId=230b270c-3055-6fae-9336-ff0000e88380&mediaType=0&propertyLocalization=0
Inner Exception --------------- Type : System.Xml.XmlException,
System.Xml, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089 Message : End element 'PropertyValue'
from namespace '' expected. Found element 'item' from namespace ''.
Source : System.Runtime.Serialization Help link : LineNumber : 0
LinePosition : 0 SourceUri : Data :
System.Collections.ListDictionaryInternal TargetSite : Void
ThrowXmlException(System.Xml.XmlDictionaryReader, System.String,
System.String, System.String, System.String) HResult : -2146232000
Stack Trace : at
System.Xml.XmlExceptionHelper.ThrowXmlException(XmlDictionaryReader
reader, String res, String arg1, String arg2, String arg3)
at System.Xml.XmlExceptionHelper.ThrowEndElementExpected(XmlDictionaryReader
reader, String localName, String ns)
at System.Xml.XmlBaseReader.ReadEndElement()
at System.Xml.XmlBaseReader.ReadElementContentAsString()
at ReadWcfControlPropertyFromJson(XmlReaderDelegator , XmlObjectSerializerReadContextComplexJson , XmlDictionaryString ,
XmlDictionaryString[] )
at System.Runtime.Serialization.Json.JsonClassDataContract.ReadJsonValueCore(XmlReaderDelegator
jsonReader, XmlObjectSerializerReadContextComplexJson context)
at System.Runtime.Serialization.Json.XmlObjectSerializerReadContextComplexJson.ReadDataContractValue(DataContract
dataContract, XmlReaderDelegator reader)
at System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(XmlReaderDelegator
reader, String name, String ns, Type declaredType, DataContract&
dataContract)
at System.Runtime.Serialization.XmlObjectSerializerReadContextComplex.InternalDeserialize(XmlReaderDelegator
xmlReader, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle,
String name, String ns)
at ReadArrayOfWcfControlPropertyFromJson(XmlReaderDelegator , XmlObjectSerializerReadContextComplexJson , XmlDictionaryString ,
XmlDictionaryString , CollectionDataContract )
at System.Runtime.Serialization.Json.JsonCollectionDataContract.ReadJsonValueCore(XmlReaderDelegator
jsonReader, XmlObjectSerializerReadContextComplexJson context)
at System.Runtime.Serialization.Json.XmlObjectSerializerReadContextComplexJson.ReadDataContractValue(DataContract
dataContract, XmlReaderDelegator reader)
at System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(XmlReaderDelegator
reader, String name, String ns, Type declaredType, DataContract&
dataContract)
at System.Runtime.Serialization.XmlObjectSerializerReadContextComplex.InternalDeserialize(XmlReaderDelegator
xmlReader, Type declaredType, DataContract dataContract, String name,
String ns)
at System.Runtime.Serialization.Json.DataContractJsonSerializer.InternalReadObject(XmlReaderDelegator
xmlReader, Boolean verifyObjectName)
at System.Runtime.Serialization.XmlObjectSerializer.ReadObjectHandleExceptions(XmlReaderDelegator
reader, Boolean verifyObjectName, DataContractResolver
dataContractResolver)
I don't have a JSON or JS file for the view. When I use the variation of this for single item select all works good.
It turns out the value for the sf-selected-ids attribute needs to be in JSON array format. e.g. [ productId1, productId2, productId3]. Otherwise the backend service throws that exception. However, the selector by itself creates the string as product1, product2, product3. I.e. without the brackets. (You can see this in the advanced view of the designer).
So here are the detailed steps:
Here is the selector in the designer view (DesignerView.Simple.cshtml):
<sf-list-selector sf-dynamic-items-selector sf-provider="properties.ProductProviderName.PropertyValue" sf-item-type="properties.ProductType.PropertyValue" sf-multiselect="true" sf-sortable="true" sf-master="true" sf-selected-ids="productIds" />
You'll need the designer JS file to do the JSON conversion back and forth. So I save this in the MVC/Scripts/[WidgetName]/designer-simple.json: (Simple is the name of the designer view)
(function ($) {
var designerModule = angular.module('designer');
angular.module('designer').requires.push('sfSelectors');
designerModule.controller('SimpleCtrl', ['$scope', 'propertyService', function ($scope, propertyService) {
$scope.feedback.showLoadingIndicator = true;
propertyService.get().then(function (data) {
if (data) {
$scope.properties = propertyService.toAssociativeArray(data.Items);
}
},
function (data) {
$scope.feedback.showError = true;
if (data)
$scope.feedback.errorMessage = data.Detail;
}).finally(function () {
$scope.feedback.showLoadingIndicator = false;
});
$scope.$watch('properties.ProductIds.PropertyValue', function (newValue, oldValue) {
if (newValue) {
$scope.productIds = JSON.parse(newValue);
}
});
$scope.$watch('productIds', function (newValue, oldValue) {
if (newValue) {
$scope.properties.ProductIds.PropertyValue = JSON.stringify(newValue);
}
});
}]);
})(jQuery);
Lastly I added a DesignerView.Simple.json file in the same folder as DesignerView.Simple.cshtml:
{
"priority": 1,
"scripts": [
"client-components/selectors/common/sf-selected-items-view.js"
],
"components" : ["sf-dynamic-items-selector"]
}
The widget controller has a ProductIds property. Its values will be in format [productId1, productId2, etc.]. I used a JSON deserializer to get an array of products for the controller Index action:
public class ProductListController : Controller
{
private string productProviderName = WebConfigurationManager.AppSettings["productProviderName"];
private string productTypeName = WebConfigurationManager.AppSettings["productTypeName"];
public string ProductIds { get; set; }
public string ProductType
{
get { return productTypeName; }
set { productTypeName = value; }
}
public string ProductProviderName
{
get { return productProviderName; }
set { productProviderName = value; }
}
public ActionResult Index()
{
var selectedProducts = string.IsNullOrEmpty(this.ProductIds) ? new Guid[0] : JsonConvert.DeserializeObject<Guid[]>(this.ProductIds);
// ... rest of your controller index action
}
}
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
});
}
}
I would like to set the bottom Corner Radius of a ListView Item for just the "last" item in the list. I've attempted to do so with a Converter (which in fact finds the last row), but to no avail.
The desirable effect is when the Converter returns true after finding the last item in the ListView, the border CornerRadius on the last ListViewItem is set to CornerRadius="0,0,10,10". For all other items in the ListView, CornerRadius="0,0,0,0"
What I've done so far.
The Converter...
public class IsLastItemConverter : IValueConverter
{
#region IValueConverter Members
public object TrueValue { get; set; }
public object FalseValue { get; set; }
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
ListViewItem item = value as ListViewItem;
ListView listView = ItemsControl.ItemsControlFromItemContainer(item) as ListView;
if (listView != null)
{
int index = listView.ItemContainerGenerator.IndexFromContainer(item);
if (index == listView.Items.Count - 1)
{
TrueValue = true;
return (bool)TrueValue;
}
else
{
FalseValue = false;
return (bool)FalseValue;
}
}
FalseValue = false;
return (bool)FalseValue;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
// Just Convert Back
return true;
}
#endregion
}
The XAML...
<local:IsLastItemConverter x:Key="lastItemConverter"
TrueValue="0,0,10,10" FalseValue="0,0,0,0"/>
<DataTemplate x:Key="CellContentTemplate">
<Border
x:Name="CellContentBorder"
Background="{StaticResource GrayCharcoal}"
BorderThickness="4,4,4,4"
BorderBrush="{StaticResource Gray}"
CornerRadius="{Binding Converter={StaticResource lastItemConverter},
RelativeSource={RelativeSource AncestorType={x:Type ListViewItem}}}"
HorizontalAlignment="Left"
Width="210"
Height="50"
ContextMenu="{StaticResource MarginalUnitsContextMenu}"
ScrollViewer.VerticalScrollBarVisibility="Hidden">...
Thoughts and ideas much appreciated - Glenn
I would try returning a real CornerRadius instead of a string (which, you are essentially returning).
My assumption is following:
When passing as string "0,0,0,0" in xaml as CornerRadius, the appropriate Converter is used to convert this string in to a CornerRadius struct.
As you are now using a custom converter, possibly the string to CornerRadius Converter is not anymore used...
EDIT (in light of Clemens answer): The Converter for the property does seem to kick in when a string was returned from the custom converter associated with the binding!
This is because CornerRadius has an associated TypeConverter:
[TypeConverterAttribute(typeof(CornerRadiusConverter))]
public struct CornerRadius : IEquatable<CornerRadius>
which handles this conversion. This converter handles the Conversion from string to CornerRadius.
(Taken from http://msdn.microsoft.com/en-us/library/system.componentmodel.typeconverterattribute.aspx )