C# Xamarin Forms 从 ViewModel 填充 CollectionView 始终为 null

C# Xamarin Forms Populating CollectionView from ViewModel is always null

我正在尝试从 ViewModel 填充集合视图,但是当我尝试将数据绑定到集合视图时,ViewModel 为空。

xaml.cs 文件

ObservableCollection<ReportsClass> newKidList = new ObservableCollection<ReportsClass>();

        public ReportsViewModel viewmodel { get; set; }

        public ReportsPage()
        {

            InitializeComponent();

            viewmodel = new ReportsViewModel();

            this.BindingContext = viewmodel;

            PreviousDateRange.CornerRadius = 20;
            NextDateRange.CornerRadius = 20;

            DateTime firstDate = currentDate.StartOfWeek(DayOfWeek.Sunday);
            DateTime secondDate = currentDate.AddDays(7).StartOfWeek(DayOfWeek.Saturday);

            DateRange.Text = firstDate.ToString("MMMM d") + " - " + secondDate.ToString("MMMM d");

            Kids.SetBinding(ItemsView.ItemsSourceProperty, nameof(viewmodel.kids));


        }

这是我的视图模型

public class ReportsViewModel
    {

        public ObservableCollection<ReportsClass> kids { get; set; }

        FirebaseStorageHelper firebaseStorageHelper = new FirebaseStorageHelper();

        WebServiceClass webServiceClass = new WebServiceClass();

        DateTime currentDate = DateTime.Now;

        public ReportsViewModel()
        {
            GetKids();
        }

        public async void GetKids()
        {
            var parentId = await SecureStorage.GetAsync("parentid");

            kids = await webServiceClass.Reports(Convert.ToInt32(parentId), currentDate.StartOfWeek(DayOfWeek.Sunday), currentDate.AddDays(7).StartOfWeek(DayOfWeek.Saturday));

        }
    }

这是获取视图模型数据的方法

public async Task<ObservableCollection<ReportsClass>> Reports(int parentid, DateTime startDate, DateTime endDate)
        {
            var content = new FormUrlEncodedContent(new[]
            {
                new KeyValuePair<string, string>("parentid", parentid.ToString()),
                new KeyValuePair<string, string>("startDate", startDate.ToString("yyyy-MM-dd H:mm:ss")),
                new KeyValuePair<string, string>("endDate", endDate.ToString("yyyy-MM-dd"))
            });

            var response = await client.PostAsync(string.Format("https://example.com/api/index.php?action=reports"), content);

            var responseString = await response.Content.ReadAsStringAsync();

            ObservableCollection<ReportsClass> items = JsonConvert.DeserializeObject<ObservableCollection<ReportsClass>>(responseString);

            return items;

        }

我做错了什么?我这样做的目的是为了更新 collectionview

中的项目

这是我的 ReportsClass

public class ReportsClass
{

    public ReportsClass(string firstName)
    {
        first_name = firstName;
    }

    public string first_name { get; set; }

}

将这行代码移到构造函数的末尾

this.BindingContext = viewmodel;

选项A:

  1. 修复 Kids.SetBinding 的语法,使其不获取 null。参考CLASS ReportsViewModel,不是INSTANCE viewmodel:
    Kids.SetBinding(ItemsView.ItemsSourceProperty, nameof(ReportsViewModel.kids));
  1. 孩子们仍然不会出现在列表中。要修复,kids 需要 OnPropertyChanged:
    public ObservableCollection<ItemModel> kids {
        get => _kids;
        set {
            _kids = value;
            OnPropertyChanged();
        }
    }
    private ObservableCollection<ItemModel> _kids;
  1. 参见选项 B 中的其他代码。根据需要进行调整。

  2. 当您需要 XAML 来查看 DYNAMIC 更改时,您需要 OnPropertyChanged. This is an implementation of INotifyPropertyChanged。将此调用添加到 ReportsClass 的属性(XAML 绑定到):

// Inheriting from `BindableObject` is one way to obtain OnPropertyChanged method.
public class ReportsClass : Xamarin.Forms.BindableObject
{

    public ReportsClass(string firstName)
    {
        first_name = firstName;
    }

    public string first_name {
        get => _first_name;
        set {
            _first_name = value;
            // This tells XAML there was a change.
            // Makes "{Binding first_name}" work dynamically.
            OnPropertyChanged();
        }
    }
    private string _first_name;

}

选项 B:

在任何地方都找不到正确的答案,所以这里有一个完整的示例,以供将来参考:

  1. 移除Kids.SetBinding(...)。 (它可以按照 OPTION A 所示进行修复,但在 XAML 中更容易正确,因此下面我将其显示在 XAML 中。)

  2. 从页面到 VM
  3. Bindings。请参阅下面的 xaml

  4. 用 setter 创建 ObservableCollection 并执行 OnPropertyChanged。这会在列表准备就绪时通知 XAML,因此页面会更新。 (这是 Jason 提到的 INotifyPropertyChanged 的实现。)

  5. 使用Device.BeginInvokeOnMainThread(async ()创建一个async上下文,即queued to 运行 after constructor returns. (这修复了 Jason 提到的问题,即构造函数不是 async 上下文,因此不应直接调用 async 方法,例如 QueryItemsAsync,或者您的 GetKids.) 这个比较靠谱

PageWithQueryData.xaml:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="TestXFUWP.PageWithQueryData">
    <ContentPage.Content>
        <StackLayout>
            <CollectionView ItemsSource="{Binding Items}">
                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <StackLayout>
                            <Label Text="{Binding Name}" />
                        </StackLayout>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
                <CollectionView.EmptyView>
                    <Grid>
                        <Label Text="Loading ..." FontSize="24" TextColor="Blue" BackgroundColor="LightBlue" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" />
                    </Grid>
                </CollectionView.EmptyView>
            </CollectionView>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

PageWithQueryData.xaml.cs:

public partial class PageWithQueryData : ContentPage
{
    public PageWithQueryData()
    {
        InitializeComponent();

        // ... other initialization work here ...
        // BUT remove `Kids.Binding(...);` line. See XAML: `ItemsSource="{Binding Items}"`.

        BindingContext = new VMWithQueryData();
    }
}

VMWithQueryData.cs:

class VMWithQueryData : Xamarin.Forms.BindableObject
{
    public VMWithQueryData()
    {
        // Start an async task to query.
        Xamarin.Forms.Device.BeginInvokeOnMainThread(async () => {
            await QueryItemsAsync();
        });

        // Alternative implementation: Start a background task to query.
        //QueryItemsInBackground();
    }

    public ObservableCollection<ItemModel> Items {
        get => _items;
        set {
            _items = value;
            OnPropertyChanged();
        }
    }
    private ObservableCollection<ItemModel> _items;


    private async Task QueryItemsAsync()
    {
        var names = new List<string> { "One", "Two", "Three" };
        bool queryOneAtATime = false;// true;
        if (queryOneAtATime) {
            // Show each item as it is available.
            Items = new ObservableCollection<ItemModel>();
            foreach (var name in names) {
                // Simulate slow query - replace with query that returns one item.
                await Task.Delay(1000);
                Items.Add(new ItemModel(name));
            }
        } else {
            // Load all the items, then show them.
            // Simulate slow query - replace with query that returns all data.
            await Task.Delay(3000);
            var items = new ObservableCollection<ItemModel>();
            foreach (var name in names) {
                items.Add(new ItemModel(name));
            }
            Items = items;
        }
    }

    // Alternative implementation, using a background thread.
    private void QueryItemsInBackground()
    {
        Task.Run(() => {
            var names = new List<string> { "One", "Two", "Three" };
            bool queryOneAtATime = false;// true;
            if (queryOneAtATime) {
                // Show each item as it is available.
                Items = new ObservableCollection<ItemModel>();
                foreach (var name in names) {
                    // Simulate slow query - replace with query that returns one item.
                    System.Threading.Thread.Sleep(1000);
                    Items.Add(new ItemModel(name));
                }
            } else {
                // Load all the items, then show them.
                // Simulate slow query - replace with query that returns all data.
                System.Threading.Thread.Sleep(3000);
                var items = new ObservableCollection<ItemModel>();
                foreach (var name in names) {
                    items.Add(new ItemModel(name));
                }
                Items = items;
            }
        });
    }
}

ItemModel.cs:

public class ItemModel
{
    public ItemModel(string name)
    {
        Name = name;
    }

    public string Name { get; set; }
}

这也演示了 <CollectionView.EmptyView> 在查询数据时向用户显示消息。

为了完整起见,我提供了一个替代方法 QueryItemsInBackground,它使用后台线程而不是 async 方法。两种方法都适用。

注意继承自 Xamarin.Forms.BindableObject。这是获取 INotifyPropertyChanged 实现的一种方法。您可以使用任何其他 MVVM 库或技术。