Changing ListView's content per-item?

4 posts, 0 answers
  1. Alex
    Alex avatar
    2 posts
    Member since:
    Dec 2018

    Posted 08 Apr 2019 Link to this post

    I have a scenario where I have "dynamic" content in a listview. I am making a sort of chat application where you can send hyperlinks, images, videos, etc.

    I have a template defined in XAML for an incoming message and outgoing message, but I need to be able to say "this message has an image", "this message has a link", "this message has a text body with a link in the middle of it", "this message has both text and 2 images", etc.

    How can I dynamically set what content is in each listview item without having to define a bunch of things that are IsVisible="true/false"?

  2. Lance | Manager Technical Support
    Admin
    Lance | Manager Technical Support avatar
    1193 posts

    Posted 08 Apr 2019 Link to this post

    Hi Alex,

    Dynamically updating an existing item's content after it's been added is not really the best way to go. Instead you would have already decided what type of item will be added and choose the appropriate template for that message using the TemplateSelector

    In the documentaiotn, you'll see different types of classes for message. For example, we ship TextMessage out of the box because it's a guranteed message type across the available platforms.,

    The control was explicitly designed as platform and model agnostic so that you have the flexibility to define the Message class that will be used for the service. It's the ItemTemplateSelector determines what template to use in the list dependeing on what kind of message it is.

    I recommend reviewing our four main examples to gain a better understanding of how this works. A good place to start is the TravelAssistant example because it has a Template selector that returns a SummaryTemplate, FlightTemplate or WaitingForBotTemplate depending on the ChatItem's content.

    In your case you could define a ImageMessageTemplate, VideoMessageTemplate, LinkMessageTemplate and so forth. As the items is pulled in from the data source, you explicitly put it in the backing collection as that message type.

    Additional Guidance with Demo

    You can extend TextMessage class and add your own properties to distinguish a particular message type from another. This way you can have custom templates for everythign and return the appropriate one.

    Here's a simple example in which I extend TextMessage with special properties to let me know it's a ImageMessage, a WebUrlMessage. 

    public class ImageMessage : TextMessage
    {
        public string ImageUrl { get; set; }
    }
     
    public class WebUrlMessage : TextMessage
    {
        public string WebUrl { get; set; }
    }

    When the data comes in from the service, you instantiate the appropriate message type for that item. In the example below I'm just randomly choosing the message type, but you should be able to determine what type of message it is at that type because of the incoming content.

    private void OnBotMessageReceived(string message)
    {
        Device.BeginInvokeOnMainThread(() =>
        {
            // I'm just randomly choosing the message type, but you should
            // be able to determine which class to instantiate depending on your incoming content
     
            var nextRandom = rand.Next(1, 100);
     
            if (nextRandom % 3 == 0)
            {
                chat.Items.Add(new WebUrlMessage
                {
                    Author = botAuthor,
                    Text = "Hey, check out this website!",
                    WebUrl = "https://www.telerik.com"
                });
            }
            else if (nextRandom % 2 == 0)
            {
                chat.Items.Add(new ImageMessage
                {
                    Author = botAuthor,
                    Text = "Hey, check out this photo!",
                    ImageUrl = "http://www.goodandmore.com/wp-content/uploads/2018/04/Computer.jpeg"
                });
            }
            else
            {
                chat.Items.Add(new TextMessage
                {
                    Author = botAuthor,
                    Text = $"I know you said {message}, but where are you going for breakfast?",
                });
            }
        });
    }


    Now you can define the template selector, using the Type of the message to determine which template to return:

    public class MyChatItemTemplateSelector : ChatItemTemplateSelector
    {
        public DataTemplate NormalTemplate { get; set; }
        public DataTemplate ImageTemplate { get; set; }
        public DataTemplate WebTemplate { get; set; }
     
        protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
        {
            if (item is ImageMessage)
            {
                return ImageTemplate;
            }
            else if (item is WebUrlMessage)
            {
                return WebTemplate;
            }
            else if(item is TextMessage) // make sure TextMessage is last because all other inherit from this
            {
                return NormalTemplate;
            }
     
            return base.OnSelectTemplate(item, container);
        }
    }

    Finally, here's what all of this comes together as in the UI:

    <ContentPage ...>
        <ContentPage.Resources>
            <DataTemplate x:Key="MyNormalMessageTemplate">
                <StackLayout>
                    <Label Text="{Binding Text}"/>
                </StackLayout>
            </DataTemplate>
     
            <DataTemplate x:Key="MyImageMessageTemplate">
                <StackLayout>
                    <Label Text="{Binding Text}"/>
                    <Image Source="{Binding ImageUrl}" HeightRequest="200"/>
                </StackLayout>
            </DataTemplate>
     
            <DataTemplate x:Key="MyWebUrlMessageTemplate">
                <StackLayout>
                    <Label Text="{Binding Text}"/>
                    <WebView Source="{Binding WebUrl}" HeightRequest="400"/>
                </StackLayout>
            </DataTemplate>
     
            <portable:MyChatItemTemplateSelector x:Key="MyMessageTemplateSelector"
                                NormalTemplate="{StaticResource MyNormalMessageTemplate}"
                                ImageTemplate="{StaticResource MyImageMessageTemplate}"
                                WebTemplate="{StaticResource MyWebUrlMessageTemplate}"/>
        </ContentPage.Resources>
     
        <Grid>
            <telerikConversationalUI:RadChat x:Name="chat"
                              ItemTemplateSelector="{StaticResource MyMessageTemplateSelector}"/>
        </Grid>
    </ContentPage>

    and here's what that looks like at runtime:



    Regards,
    Lance | Technical Support Engineer, Principal
    Progress Telerik
    Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
  3. Korstiaan
    Korstiaan avatar
    2 posts
    Member since:
    May 2011

    Posted 21 Jun 2019 Link to this post

    So what if hypothetically the message could change from one type to another? (e.g.you've send a message to someone (like a chat app) and have received a server message that the message's been read)
    The most logical thing in my opinion would be to simply bind a property to an icon's visibility (icon instead of a background color for added complexity on the layout side) instead of adding another template with only 1 added element.

    I've experienced that the radlistview does not always show the changes in visibility of elements when using a single template.
    If I were to use multiple itemplates, will the radlistview then automatically reload and show the newly correct template?
    Can't find any documentation explaining if that's the case.
  4. Lance | Manager Technical Support
    Admin
    Lance | Manager Technical Support avatar
    1193 posts

    Posted 21 Jun 2019 Link to this post

    Hi Korstiaan,

    You're correct, TemplateSelectors by design do not listen for property changes and the templates do not get switched after it's been rendered. If you wanted to trigger the TemplateSelector to render an item with a different template, you would remove and re-add the item.

    // If a chat message's basic type is going to change and you need a new template from the selector,
    // you can replace it in the collection
    private void UpdateItem(BaseMessage newMessage)
    {
        // 1. Find the matching item's position in the list using some sort of ID property
        var item = ChatMessages.FirstOrDefault(i => i.MessageId == newMessage.MessageId);
     
        // 2. Get the original message's position
        var index = ChatMessages.IndexOf(item);
     
        // 3. Take out the original message
        ChatMessages.Remove(item);
     
        // 4. Insert the replacement message
        ChatMessages.Insert(index, newMessage);
    }

    The resulting CollectionChanged event will trigger the TemplateSelector because a DataTemplate is needed for the item changes


    Single DataTemplate Option

    You've described the other good option with using a single item template. you can have a single DataTemplate that has UI elements for the different types of messages and show/hide/change parts of that template using properties of the message data model.

    Using the same example from my earlier post, you can combine both ImageUrl and WebUrl in the same model and use a converter to show or hide the 

    public class MyMessage : TextMessage
    {
        private string _webUrl;
        private string _imageUrl;
     
        public string WebUrl
        {
            get => _webUrl;
            set
            {
                if (_webUrl != value)
                {
                    _webUrl = value;
                    OnPropertyChanged();
                }
            }
        }
     
        public string ImageUrl
        {
            get => _imageUrl;
            set
            {
                if (_imageUrl != value)
                {
                    _imageUrl = value;
                    OnPropertyChanged();
                }
            }
        }
    }

    and the converter looks something like this:

    public class EmptyStringToVisibilityConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            // If the string is not empty return true
            if (value is string url)
            {
                if (!string.IsNullOrEmpty(url))
                {
                    return true;
                }
            }
     
            return false;
        }
     
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

    You could use a single DataTemplate like this:

    <local:EmptyStringToVisibilityConverter x:Key="UrlIsVisibleConverter"/>
     
    <DataTemplate x:Key="MyNormalMessageTemplate">
        <StackLayout>
            <Label Text="{Binding Text}" />
     
            <Image Source="{Binding ImageUrl}"
                   IsVisible="{Binding ImageUrl, Converter={StaticResource UrlIsVisibleConverter}}"
                   HeightRequest="200" />
     
            <WebView Source="{Binding WebUrl}"
                     IsVisible="{Binding ImageUrl, Converter={StaticResource UrlIsVisibleConverter}}"
                     HeightRequest="400" />
        </StackLayout>
    </DataTemplate>

    The same concept applies for anything else, like BackgroundColor, showing/hiding icons, etc. The only thing you really need to be sure of is that you're invoking property changed in some way when that message gets updated.

    For example, if you're just trying to update the values of a message that already exists, you can use the same approach I use above to locate the item:

    // This will check if the item exists and update the values
    // If it does not exist, the message will be added to the chat
    private void UpdateOrAddMessage(MyMessage incomingMessage)
    {
        // 1. Find the matching item's position in the list using some sort of ID property
        var item = ChatMessages.FirstOrDefault(i => i.MessageId == incomingMessage.MessageId);
     
        if (item == null)
        {
            // If the item is not present, just add it
            ChatMessages.Add(incomingMessage);
        }
        else
        {
            // If the item does exist, update the existing instance's values to trigger PropertyChanged notifications
            item.Text = incomingMessage.Text;
            item.ImageUrl = incomingMessage.ImageUrl;
            item.WebUrl = incomingMessage.WebUrl;
        }
    }


    PropertyChanged consideration

    If you did try this and you're not seeing any changes, it would be because the property on the model is not invoking PropertyChanged notification. Otherwise the binding in the template isn't informed of the value change and any converters (i.e. bool to color converter) wouldn't be triggered.

    If you're using our Telerik.XamarinForms.ConversationalUI.TextMessage model as the base class, we already have INotifyPropertyChanged implemented. You can just call "OnPropertyChanged() in your property setters like I do above for ImageUrl and WebUrl.


    Further Assistance

    If you have trouble, please share your code and steps to reproduce with us in a Support Ticket. You have a priority support license and can use this link to open a new ticket - Get Support (choose UI for Xamarin).

    Tip - If you do open a ticket and your repro project isn't able to connect to the messaging service, please add some sample messages in the chat service logic (instead of trying to connect). This way we'll be able to directly see the data and provide a much faster time to solution.

    Regards,
    Lance | Technical Support Engineer, Principal
    Progress Telerik
    Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
Back to Top