简体   繁体   中英

Xamarin Forms ViewModel Binding / Multiple Binding objects

Relatively new to Xamarin Forms and trying to use Realm, as well, so forgive me if this is basic and easy to solve. My brain is currently fried trying to figure this out.

I have the following models:

public class State : RealmObject
{
    [PrimaryKey]
    [MapTo("_id")]
    public ObjectId Id { get; set; } = ObjectId.GenerateNewId();
    public string StateShort { get; set; }
    public string StateName { get; set; }
}

public class SalesOrder : RealmObject
{
    [PrimaryKey]
    [MapTo("_id")]
    public ObjectId Id { get; set; } = ObjectId.GenerateNewId();
    public string ContactName { get; set; }
    public string ContactEmailAddress { get; set; }
    public string CompanyName { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public State State { get; set; }
    public string ZipCode { get; set; }
    public string County { get; set; }
}

I have a Content Page for setting up a new Sales Order, as follows:
SalesOrderNewPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="TestRealm.Views.SalesOrderNewPage">
    <ContentPage.Content>
        <ScrollView>
            <StackLayout Spacing="3" Padding="15">
                <Label Text="Contact Name" FontSize="Medium" />
                <Entry Text="{Binding ContactName, Mode=TwoWay}" FontSize="Medium" />
                <Label Text="Contact Email" FontSize="Medium" />
                <Editor Text="{Binding ContactEmailAddress, Mode=TwoWay}" AutoSize="TextChanges" FontSize="Medium" Margin="0" />
                <Label Text="Company Name" FontSize="Medium" />
                <Editor Text="{Binding CompanyName, Mode=TwoWay}" AutoSize="TextChanges" FontSize="Medium" Margin="0" />
                <Label Text="Address" FontSize="Medium" />
                <Editor Text="{Binding Address, Mode=TwoWay}" AutoSize="TextChanges" FontSize="Medium" Margin="0" />
                <Label Text="City" FontSize="Medium" />
                <Editor Text="{Binding City, Mode=TwoWay}" AutoSize="TextChanges" FontSize="Medium" Margin="0" />
                <Label Text="State" FontSize="Medium" />
                <Picker x:Name="StatePicker"
                        Title="--- select state ---"
                        ItemsSource="{Binding States}"
                        ItemDisplayBinding="{Binding StateShort}"
                        SelectedItem="{Binding SelectedState}" />
                <Label Text="Zip Code" FontSize="Medium" />
                <Editor Text="{Binding ZipCode, Mode=TwoWay}" AutoSize="TextChanges" FontSize="Medium" Margin="0" />
                <Label Text="County" FontSize="Medium" />
                <Editor Text="{Binding County, Mode=TwoWay}" AutoSize="TextChanges" FontSize="Medium" Margin="0" />
                <Label Text="Application" FontSize="Medium" />
                <Picker x:Name="ApplicationPicker"
                        Title="--- select application ---">
                    <Picker.ItemsSource>
                        <x:Array Type="{x:Type x:String}">
                            <x:String>Application 1</x:String>
                            <x:String>Application 2</x:String>
                        </x:Array>
                    </Picker.ItemsSource>
                </Picker>       
                <StackLayout Orientation="Horizontal">
                    <Button Text="Cancel" Command="{Binding CancelCommand}" HorizontalOptions="FillAndExpand"></Button>
                    <Button Text="Save" Command="{Binding SaveCommand}" HorizontalOptions="FillAndExpand"></Button>
                </StackLayout>
            </StackLayout>
        </ScrollView>
    </ContentPage.Content>
</ContentPage>

And the codebehind /.cs file for that xaml page
SalesOrderNewPage.xaml.cs

using MongoDB.Bson;
using System.Collections.Generic;
using TestRealm.Models;
using TestRealm.ViewModels;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace TestRealm.Views
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class SalesOrderNewPage : ContentPage
    {
        public SalesOrder SalesOrder { get; set; }

        public List<State> AllStates { get; set; }
        public SalesOrderNewPage()
        {
            InitializeComponent();
            var states = new List<State>()
            {
                new State { Id = ObjectId.GenerateNewId(), StateShort = "NY", StateName = "New York" },
                new State { Id = ObjectId.GenerateNewId(), StateShort = "PA", StateName = "Pennsylvania" } ,
                new State { Id = ObjectId.GenerateNewId(), StateShort = "VT", StateName = "Vermont" }
            };

            BindingContext = new SalesOrderNewViewModel();
        }

        protected override void OnAppearing()
        {
            base.OnAppearing();
        }
    }
}

And here is the View Model or the page
SalesOrderNewViewModel.cs

using MongoDB.Bson;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using TestRealm.Models;
using Xamarin.Forms;

namespace TestRealm.ViewModels
{
    public class SalesOrderNewViewModel : BaseViewModel
    {
        private string contactname;
        private string contactemailaddress;
        private string companyname;
        private string address;
        private string city;
        private string state;
        private string zipcode;
        private string county;
        private List<State> states;

        public ObservableCollection<State> States { get; set; }
        public SalesOrderNewViewModel()
        {
            States = new ObservableCollection<State>();
            SaveCommand = new Command(OnSave, ValidateSave);
            CancelCommand = new Command(OnCancel);
            this.PropertyChanged +=
                (_, __) => SaveCommand.ChangeCanExecute();
        }

        private bool ValidateSave()
        {
            return !String.IsNullOrWhiteSpace(contactname)
                && !String.IsNullOrWhiteSpace(contactemailaddress)
                && !String.IsNullOrWhiteSpace(companyname);
        }

        public string ContactName
        {
            get => contactname;
            set => SetProperty(ref contactname, value);
        }

        public string ContactEmailAddress
        {
            get => contactemailaddress;
            set => SetProperty(ref contactemailaddress, value);
        }

        public string CompanyName
        {
            get => companyname;
            set => SetProperty(ref companyname, value);
        }

        public string Address
        {
            get => address;
            set => SetProperty(ref address, value);
        }

        public string City
        {
            get => city;
            set => SetProperty(ref city, value);
        }

### ERROR HERE ###
cannot implicity convert type 'string' to 'TestRealm.Models.State'

        public State State
        {
            get => state;
            set => SetProperty(ref state, value);
        }

        public string ZipCode
        {
            get => zipcode;
            set => SetProperty(ref zipcode, value);
        }

        public string County
        {
            get => county;
            set => SetProperty(ref county, value);
        }


        public Command SaveCommand { get; }
        public Command CancelCommand { get; }

        private async void OnCancel()
        {
            // This will pop the current page off the navigation stack
            await Shell.Current.GoToAsync("..");
        }

        private async void OnSave()
        {
            SalesOrder salesOrder = new SalesOrder()
            {
                Id = ObjectId.GenerateNewId(),
                ContactName = ContactName,
                ContactEmailAddress = ContactEmailAddress,
                CompanyName = CompanyName,
                Address = Address,
                City = City,
                State = State,
                ZipCode = ZipCode,
                County = County
            };

            await DataStore.AddSalesOrderAsync(salesOrder);

            // This will pop the current page off the navigation stack
            await Shell.Current.GoToAsync("..");
        }
    }
}

And, of course, this is the BaseViewModel.cs file that I beleive most people are using with Xamarin Forms
BaseViewModel.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using TestRealm.Models;
using TestRealm.Services;
using Xamarin.Forms;

namespace TestRealm.ViewModels
{
    public class BaseViewModel : INotifyPropertyChanged
    {
        public IDataStore<SalesOrder> DataStore => DependencyService.Get<IDataStore<SalesOrder>>();

        bool isBusy = false;
        public bool IsBusy
        {
            get { return isBusy; }
            set { SetProperty(ref isBusy, value); }
        }

        string title = string.Empty;
        public string Title
        {
            get { return title; }
            set { SetProperty(ref title, value); }
        }


        protected bool SetProperty<T>(ref T backingStore, T value,
            [CallerMemberName] string propertyName = "",
            Action onChanged = null)
        {
            if (EqualityComparer<T>.Default.Equals(backingStore, value))
                return false;

            backingStore = value;
            onChanged?.Invoke();
            OnPropertyChanged(propertyName);
            return true;
        }

        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            var changed = PropertyChanged;
            if (changed == null)
                return;

            changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion
    }
}

So, I am getting an error in the SalesOrderNewViewModel.cs file on the State, as it is not a string but is actually a property of State from the Model. I am not sure how to handle this at all and have found no guidance. I am trying to use a picker to pull the id property from the State Model, and display the StateName or StateShort in the Picker. However, it should set the property of id for the State in the SalesOrder model.

Sorry so clueless.
Any help is appreciated and thank you.

So, I think that there is some confusion in your code on multiple levels.

Binding

It is expected that you get an error in your code, because you're trying to use the state variable that is a String as a backing field, but your property State is of type State , so you can't do that. So your backing field should be of the same kind of the property itself

private State state;

And this would remove the error. Another thing I have noticed is that you have SalesOrder and AllStates defined in SalesOrderNewPage.xaml.cs , but you're not using them (and probably should not if you want to follow a "pure" MVVM architecture).

Picker

The picker is a control that is used to show to display a list of items, which the user can select. This means that the property that you're binding to the ItemSource for the picker should be a List , while in your case you have a single value. If your SalesOrder can have multiple States, then probably it's convenient to modify your SalesOrder model to have a list property:

public IList<State> States { get; set; }

Anyways, I am not 100% sure of what you are trying to show to the user, so I can't be more precise with my answer, but in this other answer you can find an example of how to use a Picker to show the elements from a list. I hope that the other answer will drive you in the right direction:)

Realm

As a side note, this is not really a problem with realm, but with Xamarin Forms and bindings. Nevertheless, I can see in your code that you are using something that looks like a Repository pattern, that is you're not accessing realm directly in your ViewModel but "hiding" your data access in a dependency. This approach is discouraged with Realm because:

  1. You lose some of the advantages of using Realm, that is that all the objects are live and auto-updating.
  2. It complicates your life with threading, as realms on background threads need to be treated differently than a realm on the main thread. You can take a look at this other answer for some more details.

In the second part of this virtual Realm Meetup there is also a more detailed explanation of how to deal with threads and why the Repository pattern isn't a good idea.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM