【问题标题】:Binding Issue on child view in XamarinXamarin 中子视图的绑定问题
【发布时间】:2019-10-16 21:32:12
【问题描述】:

我有一个添加和详细信息页面的共享视图。出于某种原因,在详细信息页面中,视图模型不会绑定到此子视图(页面显示为空白,因为没有来自 api 服务的填充值)。有什么想法吗?

对此进行调试,CategoryList_activity 都有来自 web api 的数据。

如何调试这个绑定过程?

ActivityView.xaml

<ContentView xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="AthlosifyMobileApp.Views.ActivityView">
    <StackLayout Spacing="12">
        <Entry x:Name="txtName" Text="{Binding Name}" HeightRequest="40" BackgroundColor="White" Placeholder="Name" HorizontalOptions="FillAndExpand"/>
        <Entry  x:Name="txtNoOfMinutes" Keyboard="Numeric"  Text="{Binding NoOfMinutes}" BackgroundColor="White" Placeholder="NoOfMinutes" HorizontalOptions="FillAndExpand"/>
        <Entry x:Name="txtDescription" Text="{Binding Description}" HeightRequest="40" BackgroundColor="White" Placeholder="Description" HorizontalOptions="FillAndExpand"/>
        <Picker ItemsSource="{Binding CategoryList}" ItemDisplayBinding="{Binding Name}" SelectedItem="{Binding SelectedCategory}"></Picker>
    </StackLayout>
</ContentView>

ActivityView.xaml.cs

namespace AthlosifyMobileApp.Views
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class ActivityView : ContentView
    {
        public ActivityView ()
        {
            InitializeComponent ();
        }
    }
}

ActivityDetailViewModel.cs

namespace AthlosifyMobileApp.ViewModels
{
    public class ActivityDetailViewModel : ActivityBaseViewModel
    {
        public ICommand DeleteCommand { get; private set; }
        public ICommand UpdateCommand { get; private set; }

        public ActivityDetailViewModel(INavigation navigation, int selectedActivityId)
        {
            _navigation = navigation;
            _activityValidator = new ActivityValidator();
            _activity = new Activity();
            _activity.Id = selectedActivityId;
            _apiService = new ApiService();

            //DeleteCommand = new Command(async () => await HandleDeleteActivity());
            UpdateCommand = new Command(async () => await UpdateActivity());

            FetchActivityDetail();
            FetchCategories();
        }


        async void FetchActivityDetail()
        {
            _activity = await _apiService.GetActivity(_activity.Id);
        }

        async void FetchCategories()
        {
            CategoryResult categoryResult = await _apiService.GetCategories();
            CategoryList = categoryResult.Results;
        }


        async Task UpdateActivity()
        {
            _activity.OwnerId = Preferences.Get(Constant.Setting_UserId, "");
            _activity.CategoryId = SelectedCategory.Id;
            _activity.CategoryName = SelectedCategory.Name;


            var validationResults = _activityValidator.Validate(_activity);

            if (validationResults.IsValid)
            {
                bool isUserAccept = await Application.Current.MainPage.DisplayAlert("Contact Details", "Update Contact Details", "OK", "Cancel");
                if (isUserAccept)
                {
                    var response = await _apiService.UpdateActivity(_activity.Id,_activity);
                    if (!response)
                    {
                        await Application.Current.MainPage.DisplayAlert("Add Activity", "Error", "Alright");
                    }
                    else
                    {
                        await _navigation.PushAsync(new ActivityListPage());
                    }
                    await _navigation.PopAsync();
                }
            }
            else
            {
                await Application.Current.MainPage.DisplayAlert("Add Contact", validationResults.Errors[0].ErrorMessage, "Ok");
            }
        }

        public async Task HandleDeleteActivity(int id)
        {
            var alert = await Application.Current.MainPage.DisplayAlert("Warning", "Do you want to delete this item?", "Yes", "Cancel");
            if (alert)
            {
                var response = await _apiService.DeleteActivity(id);
                if (!response)
                {
                    await Application.Current.MainPage.DisplayAlert("Error", "Something wrong", "Alright");
                }
                else
                {
                    await _navigation.PushAsync(new ActivityListPage());
                }
            }
        }


    }
}

ActivityBaseViewModel.cs

namespace AthlosifyMobileApp.ViewModels
{
    public class ActivityBaseViewModel : INotifyPropertyChanged
    {
        public Activity _activity;

        public INavigation _navigation;
        public IValidator _activityValidator;
        public ApiService _apiService;

        public string Name
        {
            get
            {
                return _activity.Name;
            }
            set
            {
                _activity.Name = value;
                NotifyPropertyChanged("Name");
            }
        }

        public string Description
        {
            get { return _activity.Description; }
            set
            {
                _activity.Description = value;
                NotifyPropertyChanged("Description");
            }
        }

        public int NoOfMinutes
        {
            get { return _activity.NoOfMinutes; }
            set
            {
                _activity.NoOfMinutes = value;
                NotifyPropertyChanged("NoOfMinutes");
            }
        }

        public int CategoryId
        {
            get { return _activity.CategoryId; }
            set
            {
                _activity.CategoryId = value;
                NotifyPropertyChanged("CategoryId");
            }
        }

        public string CategoryName
        {
            get { return _activity.CategoryName; }
            set
            {
                _activity.CategoryName = value;
                NotifyPropertyChanged("CategoryName");
            }
        }

        //List<Activity> _activityList;
        InfiniteScrollCollection<Activity> _activityList;

        //public List<Activity> ActivityList
        public InfiniteScrollCollection<Activity> ActivityList
        {
            get => _activityList;
            set
            {
                _activityList = value;
                NotifyPropertyChanged("ActivityList");
            }
        }

        List<Category> _categoryList;

        public List<Category> CategoryList
        {
            get { return _categoryList; }
            set
            {
                _categoryList = value;
                NotifyPropertyChanged("CategoryList");
            }
        }

        public Category SelectedCategory
        {
            get
            {
                return _activity.SelectedCategory;
            }
            set
            {
                _activity.SelectedCategory = value;
                NotifyPropertyChanged("SelectedCategory");
            }
        }

        #region INotifyPropertyChanged       
        public event PropertyChangedEventHandler PropertyChanged;
        protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion
    }
}

ActivityDetailPage.xaml

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:AthlosifyMobileApp.Views"
             x:Class="AthlosifyMobileApp.Views.ActivityDetailPage"   
             Title="Detail Activity">
    <ContentPage.ToolbarItems>
        <ToolbarItem Command="">
            <ToolbarItem.IconImageSource>
                <FontImageSource  Glyph="&#xf1c0;" FontFamily="{StaticResource MaterialFontFamily}"/>
            </ToolbarItem.IconImageSource>
        </ToolbarItem>
        <ToolbarItem Command="{Binding UpdateCommand}">
            <ToolbarItem.IconImageSource>
                <FontImageSource Size="30" Glyph="&#xf193;" FontFamily="{StaticResource MaterialFontFamily}"/>
            </ToolbarItem.IconImageSource>
        </ToolbarItem>
    </ContentPage.ToolbarItems>
    <ContentPage.Content>
        <StackLayout  Padding="20" Spacing="12">
            <local:ActivityView />
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

ActivityDetailPage.xaml.cs

namespace AthlosifyMobileApp.Views
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class ActivityDetailPage : ContentPage
    {
        public ActivityDetailPage(int activityId)
        {
            InitializeComponent ();
            BindingContext = new ActivityDetailViewModel(Navigation, activityId);
        }
    }
}

【问题讨论】:

    标签: c# xamarin xamarin.forms


    【解决方案1】:

    根据您共享的代码,我认为您可能在活动详细信息页面上看不到任何数据,因为您正在通过未等待的异步方法 (FetchActivityData) 获取数据。顺便说一句,如果可能,应避免使用 async void 方法。没有办法捕捉/处理它们抛出的异常。

    您似乎没有在等待,因为您是从视图模型的构造函数中调用的。这里实际发生的是构造函数立即返回,而 FetchActivityDetail() 和 FetchCategories() 继续在后台运行。页面已显示,但还没有数据,因此您看不到任何显示的内容。然后,当 FetchActivityDetail 完成时,它会设置 _activity,但那是一个字段,因此不会触发 PropertyChanged 事件,因此页面不知道它需要更新。

    以下是一些建议:

    1. 不要在构造函数中执行长时间运行的进程(如获取数据)。传入现有数据(如您的活动 ID)通常是可以的,尽管如果您最终想要这样做,它可能会使使用依赖注入有点困难。

    2. 当导航到需要获取数据的视图模型时,我通常建议等到视图/虚拟机显示后再进行 api 调用。为此,我让所有视图在我的视图模型中调用 OnAppearing 方法。这很容易移动到所有东西都继承自的 BasePage 和 BaseViewModel 中。然后,您可以设置 IsBusy 状态(触发一些 UI,如微调器),并填充您的数据。它可能看起来像这样:

      public override async Task OnAppearing()
      {
          await base.OnAppearing();
      
          try
          {
              IsBusy = true;
              await FetchActivityDetail(); 
              await FetchCategories(); 
          }
          catch (Exception ex)
          {
              //handle/display error
          }
          finally 
          {
              IsBusy = false;
          }
      }
      

    另一种选择是将此方法设为在导航之前调用的方法,但这需要先创建视图模型,这与您在此处使用的导航模式不同。有一些很好的视图模型优先导航的例子,但我不会在这里讨论。

    1. 确保在获取数据时,它会设置导致 PropertyChanged 事件触发的属性,以便更新视图绑定。你不能只设置一个支持字段。

    【讨论】:

    • 第 3 点:它们都是属性,但它们只是位于 BaseViewModel 之下。支持字段是什么意思?
    • 关于其他模式,你指的是这个 - msdn.microsoft.com/en-us/magazine/dn605875.aspx ... NotifyTaskCompletion Xamarin UI in C# 吗?
    • @dcpartners 我所说的支持字段的意思是:属性通常有一个私有字段来存储值(private int _x; Public int X{get=>_x;},而属性是用于获取/设置该私有字段的公共访问器。如果设置私有字段(例如 _x = 值),则不会引发任何属性更改事件,因此绑定不会更新。您必须引发事件,手动或直接调用 RaisePropertyChanged。
    • RE:导航模式 - 不,我不是指您提到的有关 NotifyTaskCompletion 的链接。我在谈论 xamarin 表单导航。默认情况下,这是基于页面的,这意味着您在要导航到的页面中使用 pass。许多人更喜欢基于视图模型的导航模式,在这种模式下,您的 vm 只需指定要导航到的 vm,导航服务将确定是哪个页面。正如在这个库中看到的github.com/codemillmatt/codemill.vmfirstnav
    • 如果您查看我的 BaseViewModel,_activity 实际上是一个公共属性,这意味着当此代码执行时 - _activity = await _apiService.GetActivity(_activity.Id); 不应触发名称、描述等?
    【解决方案2】:

    根据您的描述,您想在 Xamarin.Forms 中绑定自定义视图,我建议您不要在自定义控件内部分配绑定,使用这个:

    <ContentView
    x:Class="demo2.simplecontrol.View1"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
    <ContentView.Content>
        <StackLayout>
            <Entry x:Name="label1" />
            <Entry x:Name="label2" />
        </StackLayout>
    </ContentView.Content>
    

    public partial class View1 : ContentView
    {
        public View1 ()
        {
            InitializeComponent ();          
        }
    
        public static readonly BindableProperty Label1Property= BindableProperty.Create(
                        nameof(Label1),
            typeof(string),
            typeof(View1),
            "",
            BindingMode.TwoWay,
            propertyChanged: (bindable, oldValue, newValue) =>
            {
                if (newValue != null && bindable is View1 control)
                {
                    var actualNewValue = (string)newValue;
                    control.label1.Text = actualNewValue;
                }
            });
    
        public string Label1 { get; set; }
    
        public static readonly BindableProperty Label2Property = BindableProperty.Create(
                        nameof(Label2),
            typeof(string),
            typeof(View1),
            "",
            BindingMode.TwoWay,
            propertyChanged: (bindable, oldValue, newValue) =>
            {
                if (newValue != null && bindable is View1 control)
                {
                    var actualNewValue = (string)newValue;
                    control.label2.Text = actualNewValue;
                }
            });
    
        public string Label2 { get; set; }
    }
    

    然后你就可以在 ContentPage 中使用这个自定义视图了。

    <ContentPage
    x:Class="demo2.simplecontrol.Page10"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:demo2.simplecontrol">
    <ContentPage.Content>
        <StackLayout>
            <Label
                HorizontalOptions="CenterAndExpand"
                Text="Welcome to Xamarin.Forms!"
                VerticalOptions="CenterAndExpand" />
            <local:View1 Label1="{Binding text1}" Label2="{Binding text2}" />
        </StackLayout>
    </ContentPage.Content>
    

    public partial class Page10 : ContentPage, INotifyPropertyChanged
    {
        private string _text1;
        public string text1
        {
            get { return _text1; }
            set
            {
                _text1 = value;
                RaisePropertyChanged("text1");
            }
        }
    
        private string _text2;
        public string text2
        {
            get { return _text2; }
            set
            {
                _text2 = value;
                RaisePropertyChanged("text2");
            }
        }
        public Page10 ()
        {
            InitializeComponent ();
            text1 = "test1";
            text2 = "test2";
            this.BindingContext = this;
        }
    
    
        public event PropertyChangedEventHandler PropertyChanged;
    
        public void RaisePropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
    

    最后,你得到来自CategoryList的web api的数据,所以你可以添加断点来检查是否有数据。

    【讨论】:

    • 谢谢。我决定删除这个“自定义”视图并将其插入到 ActivityDetailPage.xaml 中,这是同样的事情。似乎它不会触发绑定,可能是在构造函数上运行的异步方法。
    • ViewModel 中的 _activity = new Activity(); 会有问题吗?当 API 完成任务并返回 _activity 时,它应该触发更改吗?
    • @dcpartners,我不知道你的活动是什么,但如果你想在它改变时更新,你可以尝试调用 NotifyPropertyChanged 事件。
    • 我应该这样做然后一次_activity = await _apiService.GetActivity(_activity.Id); ...我需要拆分:Name = _activity.Name; Description = _activity.Description; 来触发绑定?
    • @dcparthers,我猜name属性和我的代码text1一样,它绑定到customview,所以我认为你需要拆分出来。
    【解决方案3】:

    '我不确定,但显然该页面正在更改您的视图的绑定上下文。

    在视图的 OnBindingContextChanged 重写方法中设置断点并对其进行调试。如果确认,请从您的页面实例化您的视图模型。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-01-28
      • 2014-05-17
      • 1970-01-01
      • 2011-08-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多