问题
I have a list of chats on the left and messages for a given chat on the right.
I want to have the MessageList to scroll to the bottom whenever it appears or gets its data updated. How can I do this?
My code is based off of Microsoft's Master/Detail view example: https://github.com/Microsoft/Windows-universal-samples/blob/master/Samples/XamlMasterDetail/cs/MasterDetailPage.xaml
The xaml page:
<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>
回答1:
I think it's the key point that your ListView
is inside of the ContentTemplate
of ContentPresenter
.
Usually we can use 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 to get this 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;
}
My sample is like this:
<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>
The ChatEntity
class and MessageEntity
class are like this:
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; }
}
and my ChatDataTemplateSelector
is like this:
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;
}
}
Firstly I loaded the ChatList
in the left ListView
, and in the SelectionChanged
event of the left ListView
, I loaded the MessageList
, this will ensure the MessageList
be updated. It's like manually refreshing ObservableCollection
(MessageList
) for the right ListView
each time you selected a item in the left ListView
. But you can also add data to the MessageList
in other time, and add data to it whenever there is a new message. The ObservableCollection
can automatically get refresh. Here is my code:
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());
}
Data in my sample are all fake. The sample looks a little complex, but it's actually very simple, just use VisualTreeHelper
to find the ListView
and use its ScrollIntoView
method to scroll to the last item.
来源:https://stackoverflow.com/questions/37711185/windows-uwp-how-to-programmatically-scroll-listview-in-contenttemplate