UWP Problems Setting GridView Items source using x

2019-06-04 20:22发布

问题:

I am trying to populate my gridview with Photos from the Pictures library using data virtualization and compiled binding.

I've taken the Microsoft UWP (Data Virtualization Sample) and using its FileSource as my base, I modified it to use my own Picture object and tried to apply it to my UWP app. All I am getting is a blank page, and the designer is throwing an exception.

I want to use x:Bind to bind to my data source object in my model as I am using MVVM and don't want code-behind.

I couldn't get this to work in my app so I wrote a small test app that isn't even MVVM and tried to use x:Bind with my data source as an object in the code behind and it fails as to bind to the collection as well.

I can get this work with my Picture object if I set the gridview's source directly in my code-behind (which is what the sample is doing).

 async void initdata()
 {
    StorageLibrary pictures = await StorageLibrary.GetLibraryAsync(KnownLibraryId.Pictures);
    string path = pictures.SaveFolder.Path;

    FileDataSource ds = await FileDataSource.GetDataSoure(path);
    if (ds.Count > 0)
    {
        PicturesGrid.ItemsSource = ds;
    }
    else
    {
        MainPage.Current.NotifyUser("Error: The pictures folder doesn't contain any files", NotifyType.ErrorMessage);
    }
}

The FileDataSource is defined as follows:

/// <summary>
/// A custom datasource over the file system that supports data virtualization
/// </summary>
public class FileDataSource : INotifyCollectionChanged, System.Collections.IList, IItemsRangeInfo
{
   ...
}

In my code, I have created the PicturesCollection as a property:

public sealed partial class MainPage : Page
{
    public FileDataSource _PicturesCollection;
    public FileDataSource PicturesCollection { get; private set; }
    public MainPage()
    {
        this.InitializeComponent();
        PicturesGrid.ContainerContentChanging += PicturesGrid_ContainerContentChanging;
        PicturesCollection = null;
        initdata();
    }

    private void PicturesGrid_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
    {
        if (!args.InRecycleQueue)
        {
            // Sets a textblock in each item indicating its index
            //FrameworkElement ctr = (FrameworkElement)args.ItemContainer.ContentTemplateRoot;
            //if (ctr != null)
            //{
            //    TextBlock t = (TextBlock)ctr.FindName("idx");
            //    t.Text = args.ItemIndex.ToString();
            //}
        }
    }

    async void initdata()
    {
        StorageLibrary pictures = await StorageLibrary.GetLibraryAsync(KnownLibraryId.Pictures);
        string path = pictures.SaveFolder.Path;

        _PicturesCollection = await FileDataSource.GetDataSoure(path);

        if (_PicturesCollection.Count > 0)
        {
            PicturesCollection = _PicturesCollection;
            //PicturesGrid.ItemsSource = ds;
        }

    }
}

and bound it to my GridView:

<Grid Grid.Row="1">
    <GridView x:Name="PicturesGrid"
              SelectionMode="Single"
              ShowsScrollingPlaceholders="False"
              ItemsSource="{x:Bind PicturesCollection}">
        <GridView.ItemTemplate>
            <DataTemplate x:DataType="local:Picture" >
                <Grid Width="200" Height="80">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>
                    <Border Grid.RowSpan="2" Background="DimGray" Opacity="0.8" />

                    <Image Width ="130"
                           HorizontalAlignment="Center"
                           Stretch="Uniform"
                           Source="{x:Bind ImageThumbNail, Converter ={StaticResource StorageItemThumbnailoImageSourceConverter}, Mode=OneWay}" />

                    <TextBlock Grid.Row="1"
                               MaxHeight="30"
                               Text="{x:Bind Name}"
                               Foreground="White"
                               HorizontalAlignment="Center"
                               TextTrimming="CharacterEllipsis"/>
                </Grid>
            </DataTemplate>
        </GridView.ItemTemplate>
    </GridView>
</Grid>

This gives me a blank page, but if I set it in code-behind, it works. Can anyone tell me why this is so? What am I missing?

回答1:

The problem here is that when your page loaded, your PicturesCollection property is not set, so your PicturesGrid's ItemsSource is null and you can sell noting is you page.

In the constructor of your MainPage, you are using initdata method to get all data. However this method is async void and you didn't wait for its finishing. Actually, we also can't use await in constructor. So when your page loaded, the execution of await FileDataSource.GetDataSoure(path); may be not finished, you PicturesCollection property is still null here, but your PicturesGrid's ItemsSource has bound to your PicturesCollection property. Thus, the ItemsSource is null and you can see nothing. Although your PicturesCollection property will be set to the real data later, but you didn't implement property-change notification for your PicturesCollection property. And for x:Bind the default Mode is OneTime, so your PicturesGrid's ItemsSource will always be null.

To fix this issue, you can implement property-change notification for PicturesCollection property like following:

public sealed partial class MainPage : Page, INotifyPropertyChanged
{
    private FileDataSource _PicturesCollection;

    public event PropertyChangedEventHandler PropertyChanged;

    public FileDataSource PicturesCollection
    {
        get
        {
            return _PicturesCollection;
        }
        set
        {
            if (_PicturesCollection != value)
            {
                _PicturesCollection = value;
                NotifyPropertyChanged();
            }
        }
    }

    private void NotifyPropertyChanged([CallerMemberName] String propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    ...

    private async void initdata()
    {
        StorageLibrary pictures = await StorageLibrary.GetLibraryAsync(KnownLibraryId.Pictures);
        string path = pictures.SaveFolder.Path;

        var source = await FileDataSource.GetDataSoure(path);

        if (source.Count > 0)
        {
            PicturesCollection = source;
        }
    }
}

And in XAML, set the Mode of x:Bind to OneWay like:

<GridView x:Name="PicturesGrid"
          SelectionMode="Single"
          ShowsScrollingPlaceholders="False"
          ItemsSource="{x:Bind PicturesCollection, Mode=OneWay}">
    ...
</GridView>

After this, your x:Bind will work.

Updata:

If you only need one-time bindings for asynchronously-loaded data, you can force one-time bindings to be initialized by calling this.Bindings.Update(); after you've loaded data like following:

async void initdata()
{
    StorageLibrary pictures = await StorageLibrary.GetLibraryAsync(KnownLibraryId.Pictures);
    string path = pictures.SaveFolder.Path;

    _PicturesCollection = await FileDataSource.GetDataSoure(path);

    if (_PicturesCollection.Count > 0)
    {
        PicturesCollection = _PicturesCollection;

        this.Bindings.Update();
    }
}

It’s much cheaper to initialize them this way than it is to have one-way bindings and to listen for changes as it only need to add one method in your code. However, this may be not a good practice when using MVVM. For more info, please see If your data loads asynchronously in Binding object declared using {x:Bind}

Code to support {x:Bind} is generated at compile-time in the partial classes for your pages. These files can be found in your obj folder, with names like (for C#) <view name>.g.cs. The generated code includes a handler for your page's Loading event, and that handler calls the Initialize method on a generated class that represent's your page's bindings. Initialize in turn calls Update to begin moving data between the binding source and the target. Loading is raised just before the first measure pass of the page or user control. So if your data is loaded asynchronously it may not be ready by the time Initialize is called. So, after you've loaded data, you can force one-time bindings to be initialized by calling this->Bindings->Update();. If you only need one-time bindings for asynchronously-loaded data then it’s much cheaper to initialize them this way than it is to have one-way bindings and to listen for changes. If your data does not undergo fine-grained changes, and if it's likely to be updated as part of a specific action, then you can make your bindings one-time, and force a manual update at any time with a call to Update.



回答2:

To use x:Bind in UWP you should define an object of the ViewModel in XAML like this:

<Page.DataContext>
    <local:MyViewModel x:Name="MyViewModel"/>
</Page.DataContext>

And refer to it like this:

ItemsSource="{x:Bind MyViewModel.PicturesCollection}"

x:Bind can only bind from a ViewModel object that is predefined in XAML.