none
ListView renders UserControl before x:Bind

    Question

  • The more I look at this, the more my unanswered question from StackOverflow looks like a defect. So, I'm posting it here for guidance.

    In a nutshell, my problem is:

    • I am rendering a collection of immutable items using a `ListView` (and `DataTemplate`) in a Windows Universal / UWP app.
    • Since the items in the collection are immutable, I'd like to avoid change notification code and use the efficient `{x:Bind Mode=OneTime}` default.
    • However, `MyUserControl` is rendered before its `UserControlViewModel` is bound. Debugging, I see a property `get` before `set`.

    How can I ensure the `UserControlViewModel` is set before it renders `OneTime`?


    A complete example follows:

    MainPage.xaml

    <Page x:Class="MyApp.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:MyApp" xmlns:sys="using:System">
    	<ListView ItemsSource="{x:Bind PageViewModel}">
    		<ListView.ItemTemplate>
    			<DataTemplate x:DataType="sys:String">
    				<local:MyUserControl UserControlViewModel="{x:Bind}" />
    			</DataTemplate>
    		</ListView.ItemTemplate>
    	</ListView>
    </Page>

    MainPage.xaml.cs

    using Windows.UI.Xaml.Controls;
    namespace MyApp {
    	public sealed partial class MainPage : Page {
    		public string[] PageViewModel { get; set; } = new string[] { "Item1", "Item2" };
    		public MainPage() { InitializeComponent(); }
            }
    }

    MyUserControl.xaml

    <UserControl x:Class="MyApp.MyUserControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    	<TextBlock Text="{x:Bind ViewModel}" />
    </UserControl>

    MyUserControl.xaml.cs

    using Windows.UI.Xaml.Controls;
    namespace MyApp {
    	public sealed partial class MyUserControl : UserControl {
    		public string UserControlViewModel { get; set; } = "Default Value";
    		public MyUserControl() { InitializeComponent(); }
    	}
    }

    The above code renders a page with two lines containing the text "Default Value"; instead my intent was to display the values "Item1" and "Item2".

    If we make the `PageViewModel` an empty `ObservableCollection<string>` and populate it later, the problem is still present. Interestingly, replacing `ListView` with `ListBox`, or removing the `ListView` entirely, will `set` before `get`, and render as intended:

    MainPage.xaml

    <Page x:Class="MyApp.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:MyApp">
    	<local:MyUserControl UserControlViewModel="{x:Bind PageViewModel[0]}" />
    </Page>

    • Edited by TwasBrillig Wednesday, November 7, 2018 5:31 PM
    Wednesday, November 7, 2018 5:27 PM

Answers

  • Hi,

    In conclusion, as far as I know, you need to make MyUserControl support late-binding as you expect. Because UserControlViewModel property value cannot be taken to be immutable in UI-virtualization.


    In that document, the most important part regarding your question is ...

    When it's unlikely that the items will be shown again, the framework re-claims the memory.

    In short, under UI-virtualized circumstance, ListView re-uses ListViewItems (that had gone out of the view by scrolling) to display other items. Therefore, you need to design ListViewItem.Content (=DataTemplate) so that it can handle re-assignment of property correctly. Needless to say, your MyUserControl with internal {x:Bind Mode=OneTime} does not process such property updates (`get`  `set`  `set`  `set` ....) correctly, therefore it cannot be used in UI-virtualization. That matters.


    If you already have implemented MyUserControl.UserControlViewModel DependencyProperty, just append Mode=OneWay so that it can be reuse-conscious. 

    <UserControl x:Class="MyApp.MyUserControl" ...>
     <TextBlock Text="{x:Bind UserControlViewModel, Mode=OneWay}" />
    </UserControl>

    Or otherwise, if you'd like to manage to use Mode=OneTime, you need to give up UI-virtualization as I wrote in the post above.


    In anyway, as you may have already noticed, UWP's ListView is much more improved than ever before by introducing several new features ({x:Bind}, UI-virtualization, ...). It can handle hundreds of items without any problems. So practically, I think you might not need to care about little overhead due to use of Mode=OneWay instead of Mode=OneTime.


    • Edited by FEC-4RP Friday, November 9, 2018 7:07 AM
    • Marked as answer by TwasBrillig Sunday, November 11, 2018 2:07 PM
    Friday, November 9, 2018 12:34 AM

All replies

  • Hello TwasBrillig,

    Perhaps, it's an expected behavior of ListView in UWP where UI-virtualization is to be applied by default. If you'd like to manage to make your code as-is work, simply disable virtualization by putting a plain StackPanel in ListView.ItemsPanel.

        <ListView ItemsSource="{x:Bind PageViewModel}">
            <ListView.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel/>
                </ItemsPanelTemplate>
            </ListView.ItemsPanel>
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="x:String">
                    <local:MyUserControl UserControlViewModel="{x:Bind}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

    By the way, ListBox also doesn't seem to work correctly in your context because of UI-virtualization. Symptom is slightly different through. Try giving more items and see what happens on scrolling down the list.

        public string[] PageViewModel { get; set; } = Enumerable.Range(1, 100).Select(i => $"Item{i}").ToArray();

    Thursday, November 8, 2018 4:49 AM
  • Thank you FEC-4RP.

    I see; "... as items approach the viewport, the framework updates the UI elements in cached item templates with the bound data objects." So virtualizing panels intentionally render item controls before binding them.

    Not that it's the point, but I don't understand why empty space wouldn't just be reserved for controls, informed by the dimensions of existing controls. After all, the "general strategy ... is to use Opacity to hide elements that don't need to be immediately visible."

    I see a warning that "heterogeneous collections ... can create a situation where it is impossible for virtualizing panels to reuse/recycle visual elements". Perhaps this footnote explains what is going on better, but it's rather indefinite, IMO.

    Rather than just hacking this, I'd prefer to understand the best-practice. Is the main issue I need to address that controls in virtualizing panels need to support late-binding and recycling? If so what is the mechanism I need to support to resolve this? Perhaps I did something wrong, but I implemented DependencyProperty on UserControlViewModel and it didn't resolve the problem.

    A followup would be appreciated.

    Thursday, November 8, 2018 3:47 PM
  • Hi,

    In conclusion, as far as I know, you need to make MyUserControl support late-binding as you expect. Because UserControlViewModel property value cannot be taken to be immutable in UI-virtualization.


    In that document, the most important part regarding your question is ...

    When it's unlikely that the items will be shown again, the framework re-claims the memory.

    In short, under UI-virtualized circumstance, ListView re-uses ListViewItems (that had gone out of the view by scrolling) to display other items. Therefore, you need to design ListViewItem.Content (=DataTemplate) so that it can handle re-assignment of property correctly. Needless to say, your MyUserControl with internal {x:Bind Mode=OneTime} does not process such property updates (`get`  `set`  `set`  `set` ....) correctly, therefore it cannot be used in UI-virtualization. That matters.


    If you already have implemented MyUserControl.UserControlViewModel DependencyProperty, just append Mode=OneWay so that it can be reuse-conscious. 

    <UserControl x:Class="MyApp.MyUserControl" ...>
     <TextBlock Text="{x:Bind UserControlViewModel, Mode=OneWay}" />
    </UserControl>

    Or otherwise, if you'd like to manage to use Mode=OneTime, you need to give up UI-virtualization as I wrote in the post above.


    In anyway, as you may have already noticed, UWP's ListView is much more improved than ever before by introducing several new features ({x:Bind}, UI-virtualization, ...). It can handle hundreds of items without any problems. So practically, I think you might not need to care about little overhead due to use of Mode=OneWay instead of Mode=OneTime.


    • Edited by FEC-4RP Friday, November 9, 2018 7:07 AM
    • Marked as answer by TwasBrillig Sunday, November 11, 2018 2:07 PM
    Friday, November 9, 2018 12:34 AM
  • Thank you for your follow-up.

    I have it working, and it's nice to put my mind to rest whether perhaps I should be doing it another way.

    Side note, all of the model-bound properties on the child must be made OneTime (in our example above, only one exists).



    • Edited by TwasBrillig Sunday, November 11, 2018 2:19 PM
    Sunday, November 11, 2018 2:15 PM
  • Also, other hacks for this issue that may be informative, or useful in various circumstances include:

    • Use a ListBox instead of ListView
    • VirtualizingStackPanel.SetVirtualizationMode(VirtualizationMode.Standard)
    • <ItemsPanelTemplate> != <VirtualizingPanel>
    • ListView.CachingStrategy = RetainElement (Xamarin only, ignored on UWP)

    Sunday, November 11, 2018 2:53 PM