Windows UWP - 如何以编程方式滚动 ContentTemplate 中的 ListView

Windows UWP - How to programmatically scroll ListView in ContentTemplate

我在左侧有聊天列表,在右侧有给定聊天的消息。

我想让 MessageList 在出现或更新数据时滚动到底部。我该怎么做?

我的代码基于 Microsoft 的 Master/Detail 视图示例: https://github.com/Microsoft/Windows-universal-samples/blob/master/Samples/XamlMasterDetail/cs/MasterDetailPage.xaml

xaml 页:

<Page
x:Class="MyApp.Pages.ChatsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyApp.Pages"
xmlns:data="using:MyApp.Model.Profile"
xmlns:vm="using:MyApp.ViewModel"
xmlns:util="using:MyApp.Util"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">

<Page.Transitions>
    <TransitionCollection>
        <NavigationThemeTransition />
    </TransitionCollection>
</Page.Transitions>


<Page.Resources>

   <util:BoolToVisibilityConverter x:Key="BoolToVisConverter" />

    <!--CollectionViewSource x:Name="Chats"
        Source="{x:Bind ViewModel}"/>
    <CollectionViewSource x:Name="Chat"
        Source="{Binding ChatViewModel, Source={StaticResource Chats}}"/>
    <CollectionViewSource x:Name="Messages"
        Source="{Binding MessageViewModel, Source={StaticResource Chat}}"/-->

    <DataTemplate x:Key="MasterListViewItemTemplate" >
        <Grid Margin="0,11,0,13" BorderBrush="Gray" BorderThickness="2">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <TextBlock Text="{Binding ChatName}" Style="{ThemeResource ChatListTitleStyle}" />

            <TextBlock
                Text="{Binding LastMessage}"
                Grid.Row="1"
                MaxLines="1"
                Style="{ThemeResource ChatListTextStyle}" />
            <TextBlock
                Text="{Binding LastSender}"
                Grid.Column="1"
                Margin="12,1,0,0"
                Style="{ThemeResource ChatListLastSenderStyle}" />
        </Grid>
    </DataTemplate>

    <DataTemplate x:Key="DetailContentTemplate">

        <ListView x:Name="MessageList" ItemsSource="{Binding Messages}" ScrollViewer.VerticalScrollMode="Auto">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <StackPanel BorderBrush="Black" BorderThickness="1" Padding="1">
                        <TextBlock Text="{Binding Message}" Style="{StaticResource NewsfeedTextStyle}"/>
                        <Image Visibility="{Binding Path=IsPhoto, Converter={StaticResource BoolToVisConverter} }" Source="{Binding Photo}" />
                        <Image Visibility="{Binding Path=IsReaction, Converter={StaticResource BoolToVisConverter} }" Width="200" Height="200" Source="{Binding Reaction}" />
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding Sender}" Style="{StaticResource NewsfeedTimestampStyle}" Margin="1"/>
                            <TextBlock Text="{Binding SentTime}" Style="{StaticResource NewsfeedTimestampStyle}" Margin="1"/>
                        </StackPanel>
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </DataTemplate>

</Page.Resources>

<Grid x:Name="LayoutRoot" Loaded="LayoutRoot_Loaded">
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="AdaptiveStates" CurrentStateChanged="AdaptiveStates_CurrentStateChanged">
            <VisualState x:Name="DefaultState">
                <VisualState.StateTriggers>
                    <AdaptiveTrigger MinWindowWidth="720" />
                </VisualState.StateTriggers>
            </VisualState>

            <VisualState x:Name="NarrowState">
                <VisualState.StateTriggers>
                    <AdaptiveTrigger MinWindowWidth="0" />
                </VisualState.StateTriggers>

                <VisualState.Setters>
                    <Setter Target="MasterColumn.Width" Value="*" />
                    <Setter Target="DetailColumn.Width" Value="0" />
                    <Setter Target="MasterListView.SelectionMode" Value="None" />
                </VisualState.Setters>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>

    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <Grid.ColumnDefinitions>
        <ColumnDefinition x:Name="MasterColumn" Width="320" />
        <ColumnDefinition x:Name="DetailColumn" Width="*" />
    </Grid.ColumnDefinitions>

    <TextBlock
        Text="Chats"
        Margin="12,8,8,8"
        Style="{ThemeResource TitleTextBlockStyle}" />

    <ListView
        x:Name="MasterListView"
        Grid.Row="1"
        ItemContainerTransitions="{x:Null}"
        ItemTemplate="{StaticResource MasterListViewItemTemplate}"
        Background="{StaticResource ApplicationPageBackgroundThemeBrush}"
        IsItemClickEnabled="True"
        ItemClick="MasterListView_ItemClick">
        <ListView.ItemContainerStyle>
            <Style TargetType="ListViewItem">
                <Setter Property="HorizontalContentAlignment" Value="Stretch" />
            </Style>
        </ListView.ItemContainerStyle>
    </ListView>

    <ContentPresenter
        x:Name="DetailContentPresenter"
        Grid.Column="1"
        Grid.RowSpan="2"
        BorderThickness="1,0,0,0"
        Padding="24,0"
        BorderBrush="{ThemeResource SystemControlForegroundBaseLowBrush}"
        Content="{x:Bind MasterListView.SelectedItem, Mode=OneWay}"            
        ContentTemplate="{StaticResource DetailContentTemplate}">
        <ContentPresenter.ContentTransitions>
            <!-- Empty by default. See MasterListView_ItemClick -->
            <TransitionCollection />
        </ContentPresenter.ContentTransitions>
    </ContentPresenter>
</Grid>

我认为关键点是你的 ListViewContentPresenterContentTemplate 里面。

通常我们可以用ListViewBase.ScrollIntoView(Object) method method to scroll ListView to the specific item, but when the ListView is inside of DataTemplate, it is unexposed. Here is a method, we can use VisualTreeHelper得到这个ListView:

public static T FindChildOfType<T>(DependencyObject root) where T : class
{
    var queue = new Queue<DependencyObject>();
    queue.Enqueue(root);
    while (queue.Count > 0)
    {
        DependencyObject current = queue.Dequeue();
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(current); i++)
        {
            var child = VisualTreeHelper.GetChild(current, i);
            var typedChild = child as T;
            if (typedChild != null)
            {
                return typedChild;
            }
            queue.Enqueue(child);
        }
    }
    return null;
}

我的样本是这样的:

<Grid.ColumnDefinitions>
    <ColumnDefinition x:Name="MasterColumn" Width="320" />
    <ColumnDefinition x:Name="DetailColumn" Width="*" />
</Grid.ColumnDefinitions>

<ListView x:Name="MasterListView" Grid.Column="0" ItemsSource="{x:Bind ChatList}" SelectionChanged="MasterListView_SelectionChanged">
    <ListView.ItemTemplate>
        <DataTemplate x:DataType="local:ChatEntity">
            <TextBlock Text="{x:Bind Member}" />
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

<ContentPresenter x:Name="DetailContentPresenter" Grid.Column="1"
                  Content="{x:Bind MasterListView.SelectedItem, Mode=OneWay}">
    <ContentPresenter.ContentTemplate>
        <DataTemplate x:DataType="local:ChatEntity">
            <Grid>
                <Grid.Resources>
                    <DataTemplate x:Key="FromMessageDataTemplate">
                        <StackPanel Orientation="Horizontal" FlowDirection="LeftToRight">
                            <TextBlock Text="{Binding Member}" Width="30" Foreground="Blue" FontWeight="Bold" />
                            <TextBlock Text=":" Width="10" Foreground="Blue" FontWeight="Bold" />
                            <TextBlock Text="{Binding Content}" Foreground="Red" />
                        </StackPanel>
                    </DataTemplate>
                    <DataTemplate x:Key="ToMessageDataTemplate">
                        <StackPanel Orientation="Horizontal" FlowDirection="RightToLeft">
                            <TextBlock Text="Me" Width="30" HorizontalAlignment="Right" Foreground="Blue" FontWeight="Bold" />
                            <TextBlock Text=":" Width="10" HorizontalAlignment="Right" Foreground="Blue" FontWeight="Bold" />
                            <TextBlock Text="{Binding Content}" HorizontalAlignment="Right" Foreground="Green" />
                        </StackPanel>
                    </DataTemplate>
                    <local:ChatDataTemplateSelector x:Key="ChatDataTemplateSelector"
                                MessageFromTemplate="{StaticResource FromMessageDataTemplate}"
                                MessageToTemplate="{StaticResource ToMessageDataTemplate}" />
                </Grid.Resources>
                <ListView ItemsSource="{x:Bind MessageList}" ItemTemplateSelector="{StaticResource ChatDataTemplateSelector}">
                    <ListView.ItemContainerStyle>
                        <Style TargetType="ListViewItem">
                            <Setter Property="HorizontalContentAlignment" Value="Stretch" />
                        </Style>
                    </ListView.ItemContainerStyle>
                </ListView>
            </Grid>
        </DataTemplate>
    </ContentPresenter.ContentTemplate>
</ContentPresenter>

ChatEntityclass和MessageEntityclass是这样的:

public class ChatEntity
{
    public string Member { get; set; }
    public ObservableCollection<MessageEntity> MessageList { get; set; }
}

public class MessageEntity
{
    public enum MsgType
    {
        From,
        To
    }

    public string Member { get; set; }
    public string Content { get; set; }
    public MsgType MessageType { get; set; }
}

我的ChatDataTemplateSelector是这样的:

public class ChatDataTemplateSelector : DataTemplateSelector
{
    public DataTemplate MessageFromTemplate { get; set; }
    public DataTemplate MessageToTemplate { get; set; }

    protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
    {
        MessageEntity msg = item as MessageEntity;
        if (msg != null)
        {
            if (msg.MessageType == MessageEntity.MsgType.From)
                return MessageFromTemplate;
            else
                return MessageToTemplate;
        }
        return null;
    }
}

首先我在左侧ListView加载了ChatList,在左侧ListViewSelectionChanged事件中,我加载了MessageList,这将确保 MessageList 得到更新。就像每次在左侧ListView中选择一个项目时,为右侧ListView手动刷新ObservableCollection(MessageList)。但是你也可以在其他时间向MessageList中添加数据,每当有新消息时向其中添加数据。 ObservableCollection 可以自动获取刷新。这是我的代码:

private ObservableCollection<MessageEntity> messageList;
private ObservableCollection<ChatEntity> ChatList;

public MainPage()
{
    this.InitializeComponent();
    messageList = new ObservableCollection<MessageEntity>();
    ChatList = new ObservableCollection<ChatEntity>();
}

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    ChatList.Add(new ChatEntity { Member = "Tom", MessageList = messageList });
    ChatList.Add(new ChatEntity { Member = "Peter" });
    ChatList.Add(new ChatEntity { Member = "Clark" });
}

private void MasterListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    messageList.Clear();
    for (int i = 0; i < 100; i++)
    {
        if (i % 2 == 0)
            messageList.Add(new MessageEntity { Member = "Tom", Content = "Hello!", MessageType = MessageEntity.MsgType.From });
        else
            messageList.Add(new MessageEntity { Content = "World!", MessageType = MessageEntity.MsgType.To });
    }
    var listView = FindChildOfType<ListView>(DetailContentPresenter);
    listView.ScrollIntoView(messageList.Last());
}

我样本中的数据都是假的。该示例看起来有点复杂,但实际上非常简单,只需使用 VisualTreeHelper 找到 ListView 并使用其 ScrollIntoView 方法滚动到最后一项。