绑定到 MvxSpinner SelectedItem 属性 无效

Binding to MvxSpinner SelectedItem property not working

我使用 MvxSpinner 在 MvvmCross for Xamarin 应用程序的组合框中显示国家/地区 phone 前缀。我可以正确地绑定到 ItemsSource 属性,所以我可以看到我的前缀列表,但是当我在绑定到 MvxSpinner 的 SelectedItem 属性 的视图模型中分配 属性 时, 它不会工作并且总是将列表中的第一个元素显示为所选项目。

我的做法如下。在我的 ViewModel 中,我从服务器获取用户数据并分配 Country 和 PhonePrefix 的属性。然后我也从服务器获取所有国家和前缀的列表,并将它们绑定到列表属性,这些属性绑定到相应的 MvxSpinners(简化)的 ItemSource 属性:

    public string PhonePrefix { get; set; }
    public string PhoneNumber { get; set; }
    public Country Country { get; set; } = new Country();

    public List<Country> Countries { get; set; } = new List<Country>();
    public List<string> Prefixes { get; set; } = new List<string>();

    private async Task GetUserData()
    {
        try
        {
            var userDataResult = await _registrationService.GetLoggedInUserData();

            if (userDataResult != null)
            {
                if (!userDataResult.HTTPStatusCode.Equals(HttpStatusCode.OK))
                {
                    Mvx.IoCProvider.Resolve<IUserDialogs>().Alert(userDataResult.Error?.Message);
                }
                else
                {

                        PhonePrefix = userDataResult.user.country_code_phone;
                        PhoneNumber = userDataResult.user.phone;
                        Country.id = userDataResult.user.person.addresses[0].country_id;
                        Country.name = userDataResult.user.person.addresses[0].country_name;
                }
            }
        }
        catch (Exception e)
        {
            Log.Error<RegistrationViewModel>("GetUserData", e);
        }
    }

    /// <summary>
    /// Populates the view model properties for Countries and Prefixes with information retrieved from the server
    /// </summary>
    private void ProcessFormData()
    {
        if (_registrationFormData != null)
        {
            Countries = _registrationFormData.Countries?.ToList();
            var userCountry = Countries?.Where(c => c.id == Country?.id).FirstOrDefault();
            Country = IsUserLogedIn && Country != null ? userCountry : Countries?[0];

            var prefixes = new List<string>();
            if (Countries != null)
            {
                foreach (var country in Countries)
                {
                    //spinner binding doesn't allow null values
                    if (country.phone_prefix != null)
                    {
                        prefixes.Add(country.phone_prefix);
                    }
                }
            }

            //we need to assign the binded Prefixes at once otherwise the binding for the ItemSource fails
            Prefixes = prefixes;
            PhonePrefix = Prefixes.Count > 0 && !IsUserLogedIn && string.IsNullOrWhiteSpace(PhonePrefix) ? Prefixes?[0] : PhonePrefix;
        }
    }

并且在 axml 布局中:

    <MvxSpinner
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:minHeight="25dp"
            android:layout_marginTop="5dp"
            android:layout_marginBottom="10dp"
            android:id="@+id/spnrCountry"                   
            local:MvxBind="ItemsSource Countries; SelectedItem Country"/>
    <MvxSpinner
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:minHeight="25dp"
            android:layout_marginBottom="10dp"
            android:id="@+id/spnrPrefix"
            local:MvxBind="ItemsSource Prefixes; SelectedItem PhonePrefix"/>

在输出 window 上,我可以看到有关 MvxBinding 的以下错误:

(MvxBind) Null values not permitted in spinner SelectedItem binding currently

我进行了调试,但我在列表或绑定到 MvxSpinner 的 ItemSource 和 SelectedItem 属性的属性中从来没有任何 Null 值。

实际上,国家/地区 ItemSource 和 SelectedItem 可以正常工作,所以如果用户将其所在国家/地区保存为阿根廷,当我加载它的数据时,微调器中的选定项目将是阿根廷。请注意,我使用了这样的国家/地区实体:

public class Country
{
    public int id { get; set; }
    public bool favorite { get; set; }
    public string name { get; set; }
    public string name_de { get; set; }
    public string code { get; set; }
    public int rzl_code { get; set; }
    public string phone_prefix { get; set; }
    public string updated_at { get; set; }
    public string created_at { get; set; }

    public override string ToString()
    {
        return name;
    }
}

我也尝试在它自己的实体中使用 phone 前缀来包装一个字符串值,但它也没有用。

有人知道我做错了什么吗?为什么它对国家有效而对前缀无效?

我用PropertyChanged.Fody.

谢谢!

它确实有效,因为在 GetUserData 中设置了 PhonePrefix,您还应该设置国家/地区。根据您的代码,国家/地区为空,这就是您从列表中选择第一项或也出现错误的原因。

关于 null 的错误是因为微调器中的 SelectedItem 不允许将 null 作为选定项。因此,如果您想显示一个空项目,一种解决方案是添加另一个具有空值的固定项目,即在 PhonePrefix 的情况下,您可以设置 string.Empty 并将其添加到 [= 的列表中20=] 并且在您的 Country 中,您可以将第一个设置为默认值或创建一个名称为 None 的存根 Country 并将其添加到国家/地区列表中。

要考虑的另一点是,当您想要更新视图时,您必须确保在主线程中通知它。您正在尝试更新另一个线程的 Task 中的 PhonePrefix,因此视图不会被注意到。

您应该通过以下方式更新 PhonePrefix

this.InvokeOnMainThread(() => PhonePrefix = userDataResult.user.country_code_phone; );

这将负责直接在主线程上执行 PhonePrefix 的设置,以便正确通知您的视图。


更新

在更好地查看你的问题和自己的答案并看到你使用 PropertyChanged.Fody 之后,我可以猜测问题实际上是你如何分配 PhonePrefix

PropertyChanged.Fody 默认行为是添加 Equality Checking 替换您的 属性 代码

public string PhonePrefix { get; set; }

类似

private string _phonePrefix;
public string PhonePrefix
{
    get
    {
        return _phonePrefix;
    }
    set
    {
        if (!String.Equals(_phonePrefix, value))
        {
            _phonePrefix = value;
            OnPropertyChanged("PhonePrefix");
        }
    }
}

所以当你在 GetUserData():

PhonePrefix = userDataResult.user.country_code_phone;

并在 ProcessFormData()

PhonePrefix = Prefixes.Count > 0 && !IsUserLogedIn && string.IsNullOrWhiteSpace(PhonePrefix) ? Prefixes?[0] : PhonePrefix;

PhonePrefix 不是 null 或空白,因此它尝试重新分配相同的值,但因为 fody 添加了相等性检查,它不会再次分配,因此它不会引发值的变化,所以视图没有得到通知。

GetUserData() 中的分配我认为它可能正在另一个线程中完成,这就是视图未收到通知的原因。 根据您所说 Country 确实在 ProcessFormData() 中得到更新,因此为了 PhonePrefix 在那个地方也得到更新,您应该只将 [DoNotCheckEquality] 属性添加到 属性 以避免相等性检查,这应该是全部。

[DoNotCheckEquality]
public string PhonePrefix { get; set; }

如果它不起作用,你也应该在主线程上添加调用(我建议你在调试中查看正在执行的方法是在哪个线程上,看看你是否确实需要在主线程上调用)。

虽然这是一个奇怪的解决方案,而且我仍然不明白为什么,但我找到了一种方法让它工作。我觉得它与异步有关。

问题出在 GetuserData() 方法中的 PhonePrefix 属性 分配,然后在 ProcessFormData() 中重新分配。所以现在有效的代码如下所示:

private string _phonePrefix;
public string PhonePrefix { get; set; }
public string PhoneNumber { get; set; }
public Country Country { get; set; } = new Country();

public List<Country> Countries { get; set; } = new List<Country>();
public List<string> Prefixes { get; set; } = new List<string>();

public override async Task Initialize()
{
    await base.Initialize();
    IsUserLogedIn = await _authService.IsUserLoggedIn();
    if (IsUserLogedIn)
    {
        //get user data from server, show user data
        await GetUserData();
    }

    //get countries, prefixes
    if (Countries.Count <= 0 || Prefixes.Count <= 0)
    {
        _registrationFormData = await _registrationService.GetRegistrationFormData();
        ProcessFormData();
    }

    AddValidationRules();
}

private async Task GetUserData()
{
    try
    {
        var userDataResult = await _registrationService.GetLoggedInUserData();

        if (userDataResult != null)
        {
            if (!userDataResult.HTTPStatusCode.Equals(HttpStatusCode.OK))
            {
                Mvx.IoCProvider.Resolve<IUserDialogs>().Alert(userDataResult.Error?.Message);
            }
            else
            {

                    //PhonePrefix = userDataResult.user.country_code_phone;
                    //can't bind it here to the public binded property because the SelectedItem binding fails. 
                    //I first assign it to a private field and then use it in the ProcessFormData method
                    _phonePrefix = userDataResult.user.country_code_phone;
                    PhoneNumber = userDataResult.user.phone;
                    Country.id = userDataResult.user.person.addresses[0].country_id;
                    Country.name = userDataResult.user.person.addresses[0].country_name;
            }
        }
    }
    catch (Exception e)
    {
        Log.Error<RegistrationViewModel>("GetUserData", e);
    }
}



    private void ProcessFormData()
    {
        if (_registrationFormData != null)
        {
            Countries = _registrationFormData.Countries?.ToList();
            var userCountry = Countries?.Where(c => c.id == Country?.id).FirstOrDefault();
            Country = IsUserLogedIn && Country != null ? userCountry : Countries?[0];

            var prefixes = new List<string>();
            if (Countries != null)
            {
                foreach (var country in Countries)
                {
                    //spinner binding doesn't allow null values
                    if (country.phone_prefix != null)
                    {
                        prefixes.Add(country.phone_prefix);
                    }
                }
            }

            //we need to assign the binded Prefixes at once otherwise the binding for the SelectedItem fails
            Prefixes = prefixes;
            if (Prefixes.Count > 0 && !IsUserLogedIn && string.IsNullOrWhiteSpace(_phonePrefix))
            {
                PhonePrefix = Prefixes?[0];
            }
            else
            {
                PhonePrefix = _phonePrefix;
            }
        }
    }

注意添加私有 属性 _phone 前缀以在 GetUserData() 中保存 phone 前缀值,然后将其值分配给 属性它在视图中绑定,PhonePrefix。

实际上我无法真正解释为什么会发生这种情况,所以如果有人能解释这种行为就太好了。

您正在使用的属性不会通知视图的更改,为此您必须使用:

string _phonePrefix; public string PhonePrefix { get => _phonePrefix; set => SetProperty(ref _phonePrefix, value); }