New to Telerik UI for .NET MAUI? Start a free 30-day trial
Chat Attachments
Updated on Nov 13, 2025
The Telerik UI for .NET MAUI Chat control allows you to send and receive attachments as part of the conversation. Attachments can include images, documents, or other file types that enhance the messaging experience.
To add attachments you have to apply the following settings:
- Set the
IsMoreButtonVisibletotrueto show the More button in the input area of the Chat control. - Bind the
SendMessageCommandto the command that sends the message. - Bind the
AttachedFilesSourceto the collection of files to be sent. This is the items source from which the chat populates theTelerik.Maui.Controls.RadChat.AttachedFilescollection, i.e. the files that are to be uploaded and have not yet been sent. - Bind the
AttachFilesCommandto the command that handles file attachment. The command executes when the files the end-user picked need to be attached/uploaded. - Set the
AttachedFileConverter. This is the converter that performs the conversion between a data item representing an attached file and aTelerik.Maui.Controls.Chat.ChatAttachedFile.
Example with AttachFilesCommand
- Define the
RadChatcomponent:
xaml
<ContentView.Resources>
<ResourceDictionary>
<local:ItemConverter x:Key="ItemConverter" />
</ResourceDictionary>
</ContentView.Resources>
<telerik:RadChat ItemConverter="{StaticResource ItemConverter}"
ItemsSource="{Binding Items}"
IsMoreButtonVisible="True"
Message="{Binding Message}"
SendMessageCommand="{Binding SendMessageCommand}"
AttachedFileConverter="{Static local:AttachedFileConverter.Instance}"
AttachedFilesSource="{Binding AttachedFiles}"
AttachFilesCommand="{Binding AttachFilesCommand, Converter={Static local:AttachFilesCommandConverter.Instance}}" />
- Add the
ViewModelwith theAttachFilesCommanddefinition:
c#
public class ChatWithAttachmentsViewModel : NotifyPropertyChangedBase
{
private int attachmentsCount;
private ObservableCollection<AttachedFileData> attachedFiles;
private Command sendMessageCommand;
private object message;
public ChatWithAttachmentsViewModel()
{
this.Me = "human";
this.Bot = "bot";
this.Items = new ObservableCollection<MessageItem>();
this.sendMessageCommand = new Command(this.ExecuteSendMessageCommand, this.CanExecuteSendMessageCommand);
this.AttachedFiles = new ObservableCollection<AttachedFileData>();
this.AttachFilesCommand = new Command(this.AttachFiles);
this.LoadDataFromService();
}
public object Me { get; }
public object Bot { get; }
public object Message
{
get => this.message;
set => this.UpdateValue(ref this.message, value);
}
public IList<MessageItem> Items { get; set; }
public ICommand AttachFilesCommand { get; }
public ICommand SendMessageCommand { get => this.sendMessageCommand; }
public ObservableCollection<AttachedFileData> AttachedFiles
{
get => this.attachedFiles;
set => this.UpdateValue(ref this.attachedFiles, value, this.OnAttachedFilesChanged);
}
private async void LoadDataFromService()
{
await DataFileService.Init();
List<PredefinedFile> files = DataFileService.predefinedFiles;
this.Items.Add(new AttachmentsItem { Author = this.Bot, Text = "Review this document and sign it, please", Attachments = this.GetAttachments(1) });
this.Items.Add(new AttachmentsItem { Author = this.Bot, Text = "Check out these files and apply the needed changes.", Attachments = this.GetAttachments(2) });
this.Items.Add(new AttachmentsItem { Author = this.Bot, Text = "I am sending the song audio and the video. Please let me know what you think", Attachments = this.GetAttachments(2) });
this.Items.Add(new AttachmentsItem { Author = this.Me, Text = "Document signed!", Attachments = this.GetAttachments(1) });
this.Items.Add(new AttachmentsItem { Author = this.Me, Text = "The files are added to the archive", Attachments = this.GetAttachments(1) });
this.Items.Add(new AttachmentsItem { Author = this.Me, Text = "The song is really good, and the video is awesome. Congrats!", Attachments = this.GetAttachments(0) });
}
private void OnAttachedFilesChanged(ObservableCollection<AttachedFileData> oldValue)
{
if (oldValue != null)
{
oldValue.CollectionChanged -= this.AttachedFiles_CollectionChanged;
}
if (this.attachedFiles != null)
{
this.attachedFiles.CollectionChanged += this.AttachedFiles_CollectionChanged;
}
this.sendMessageCommand.ChangeCanExecute();
}
private async void AttachedFiles_CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
this.sendMessageCommand.ChangeCanExecute();
if (args.Action == NotifyCollectionChangedAction.Add)
{
// We want to start the upload process for the newly added file.
foreach (AttachedFileData attachedFileData in args.NewItems)
{
await this.TryUploadFile(attachedFileData);
}
}
else if (args.Action == NotifyCollectionChangedAction.Remove)
{
// User decided to not send the file, so delete it from the server.
foreach (AttachedFileData attachedFileData in args.OldItems)
{
this.TryDeleteFile(attachedFileData);
}
}
this.sendMessageCommand.ChangeCanExecute();
}
private async Task TryUploadFile(AttachedFileData attachedFileData)
{
if (!this.AttachedFiles.Contains(attachedFileData))
{
// The item was removed before we had a chance to upload it.
return;
}
using (Stream stream = await attachedFileData.GetStream())
{
Guid guid = await DataFileService.UploadFile(stream);
if (!this.AttachedFiles.Contains(attachedFileData))
{
// The item was removed while upload was running.
DataFileService.DeleteFile(guid);
return;
}
else if (guid != Guid.Empty)
{
attachedFileData.Guid = guid;
}
}
}
private void TryDeleteFile(AttachedFileData attachedFileData)
{
DataFileService.DeleteFile(attachedFileData.Guid);
}
private void AttachFiles(object commandParameter)
{
IList<AttachedFileData> filesToAttach = (IList<AttachedFileData>)commandParameter;
foreach (AttachedFileData attachedFileData in filesToAttach)
{
this.AttachedFiles.Add(attachedFileData);
}
// Instruct the RadChat to not attempt to auto add files.
filesToAttach.Clear();
}
private ObservableCollection<AttachmentData> GetAttachments(int count)
{
ObservableCollection<AttachmentData> list = new ObservableCollection<AttachmentData>();
for (int i = 0; i < count; i++)
{
PredefinedFile file = DataFileService.predefinedFiles[this.attachmentsCount % DataFileService.predefinedFiles.Count];
AttachmentData attachmentsData = new AttachmentData { Name = file.fileName, Size = file.fileSize, Guid = file.guid, };
list.Add(attachmentsData);
this.attachmentsCount++;
}
return list;
}
private static AttachmentData CreateAttachmentData(AttachedFileData attachedFile)
{
return new AttachmentData { Name = attachedFile.Name, Size = attachedFile.Size, Guid = attachedFile.Guid, };
}
private bool CanExecuteSendMessageCommand(object arg)
{
string myMessageString = this.message as string;
if (string.IsNullOrWhiteSpace(myMessageString))
{
return true;
}
if (this.AttachedFiles.Any(file => file.Guid == Guid.Empty))
{
return false;
}
return true;
}
private void ExecuteSendMessageCommand(object obj)
{
string myMessageString = this.message as string;
this.Message = string.Empty;
ObservableCollection<AttachedFileData> attachedFiles = this.AttachedFiles;
if (attachedFiles.Count != 0)
{
// Create a new collection without deleting the uploaded files.
this.AttachedFiles = new ObservableCollection<AttachedFileData>();
ObservableCollection<AttachmentData> attachments = new ObservableCollection<AttachmentData>(attachedFiles.Select(CreateAttachmentData));
AttachmentsItem attachmentsMessage = new AttachmentsItem { Author = this.Me, Text = myMessageString, Attachments = attachments };
this.Items.Add(attachmentsMessage);
}
else
{
MessageItem message = new MessageItem { Author = this.Me, Text = myMessageString, };
this.Items.Add(message);
}
}
}
- Create a sample
MessageItemmodel:
c#
public class MessageItem : NotifyPropertyChangedBase
{
private object author;
private string text;
public object Author
{
get => this.author;
set => this.UpdateValue(ref this.author, value);
}
public string Text
{
get => this.text;
set => this.UpdateValue(ref this.text, value);
}
}
- Create
AttachmentsItemclass and define the attachments collection property:
c#
public class AttachmentsItem : MessageItem
{
private ObservableCollection<AttachmentData> attachments;
public ObservableCollection<AttachmentData> Attachments
{
get => this.attachments;
set => this.UpdateValue(ref this.attachments, value);
}
}
- Define an
AttachmentDataclass to hold the attachment information:
c#
public class AttachmentData : NotifyPropertyChangedBase
{
private string name;
private long size;
private Guid guid;
public string Name
{
get => this.name;
set => this.UpdateValue(ref this.name, value);
}
public long Size
{
get => this.size;
set => this.UpdateValue(ref this.size, value);
}
public Guid Guid
{
get => this.guid;
set => this.UpdateValue(ref this.guid, value);
}
}
- Define the custom class for the attachments file data:
c#
public class AttachedFileData : NotifyPropertyChangedBase
{
private string name;
private long size;
private Func<Task<Stream>> getStream;
private Guid guid;
public string Name
{
get => this.name;
set => this.UpdateValue(ref this.name, value);
}
public long Size
{
get => this.size;
set => this.UpdateValue(ref this.size, value);
}
public Guid Guid
{
get => this.guid;
set => this.UpdateValue(ref this.guid, value);
}
public Func<Task<Stream>> GetStream
{
get => this.getStream;
set => this.UpdateValue(ref this.getStream, value);
}
}
- Define a converter to convert a data item to a chat attachment. In general here you need to create and set up the corresponding
Telerik.Maui.Controls.Chat.ChatAttachedFilefor the given business object:
c#
public class AttachedFileConverter : IChatAttachedFileConverter
{
private static AttachedFileConverter instance;
public static AttachedFileConverter Instance => instance ??= new AttachedFileConverter();
public ChatAttachedFile ConvertToChatAttachedFile(object dataItem, ChatAttachedFileConverterContext context)
{
AttachedFileData data = (AttachedFileData)dataItem;
ChatAttachedFile chatAttachedFile = new ChatAttachedFile { Data = data, FileName = data.Name, FileSize = data.Size };
return chatAttachedFile;
}
public object ConvertToDataItem(IFileInfo fileToAttach, ChatAttachedFileConverterContext context)
{
return CreateAttachedFileData(fileToAttach);
}
internal static AttachedFileData CreateAttachedFileData(IFileInfo file)
{
return new AttachedFileData { Name = file.FileName, Size = file.FileSize, GetStream = file.OpenReadAsync, };
}
}
- Define a custom converter that converts from chat specific objects to business objects, so that the
ViewModeldoes not have to handle chat specific classes:
c#
/// <summary>
/// A custom converter that converts from chat specific objects to business objects,
/// so that the ViewModel does not have to handle chat specific classes.
/// </summary>
public class AttachFilesCommandConverter : IValueConverter
{
private static AttachFilesCommandConverter instance;
public static AttachFilesCommandConverter Instance => instance ??= new AttachFilesCommandConverter();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is ICommand command)
{
return new AttachFilesCommand(command);
}
else
{
return value;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
class AttachFilesCommand : ICommand
{
private ICommand command;
public AttachFilesCommand(ICommand command)
{
this.command = command;
this.command.CanExecuteChanged += (s, e) => this.CanExecuteChanged?.Invoke(this, e);
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
if (parameter is IList<IFileInfo> filesToAttach)
{
List<AttachedFileData> attachedFileDatas = ToAttachedFileDatas(filesToAttach);
return this.command.CanExecute(attachedFileDatas);
}
else
{
return false;
}
}
public void Execute(object parameter)
{
if (parameter is IList<IFileInfo> filesToAttach)
{
List<AttachedFileData> attachedFileDatas = ToAttachedFileDatas(filesToAttach);
this.command.Execute(attachedFileDatas);
if (attachedFileDatas.Count == 0)
{
// Instruct the RadChat to not attempt to auto add files.
filesToAttach.Clear();
}
}
else
{
throw new InvalidOperationException($"The command parameter must be of type {nameof(IList<IFileInfo>)}.");
}
}
private static List<AttachedFileData> ToAttachedFileDatas(IList<IFileInfo> filesToAttach)
{
return new List<AttachedFileData>(filesToAttach.Select(AttachedFileConverter.CreateAttachedFileData));
}
}
}
- The
ItemConverteris need in MVVM scenario as custom items are used:
c#
public class ItemConverter : IChatItemConverter
{
private static ItemConverter instance;
public static ItemConverter Instance => instance ??= new ItemConverter();
private Dictionary<object, Author> authorDict;
public ItemConverter()
{
this.authorDict = new Dictionary<object, Author>();
}
public ChatItem ConvertToChatItem(object dataItem, ChatItemConverterContext context)
{
MessageItem message = (MessageItem)dataItem;
ChatMessage chatMessage;
if (message is AttachmentsItem attachmentsItem)
{
List<ChatAttachment> chatAttachments = attachmentsItem.Attachments.Select(CreateChatAttachment).ToList();
chatMessage = new ChatAttachmentsMessage { Attachments = chatAttachments };
chatMessage.SetBinding(ChatAttachmentsMessage.TextProperty, new Binding(nameof(MessageItem.Text)) { Source = message });
}
else
{
chatMessage = new TextMessage();
chatMessage.SetBinding(TextMessage.TextProperty, new Binding(nameof(MessageItem.Text)) { Source = message });
}
chatMessage.Data = message;
chatMessage.Author = GetOrCreateAuthor(message.Author, context);
return chatMessage;
}
public object ConvertToDataItem(object message, ChatItemConverterContext context)
{
// We add a new message into the messages in the view model when the SendMessageCommand is executed, so no need to create a new data item here.
return null;
}
private static ChatAttachment CreateChatAttachment(AttachmentData attachment)
{
ChatAttachment chatAttachment = new ChatAttachment { Data = attachment, FileName = attachment.Name, FileSize = attachment.Size };
chatAttachment.GetFileStream = () => GetFileStreamTask(attachment);
return chatAttachment;
}
private static async Task<Stream> GetFileStreamTask(AttachmentData attachment)
{
if (attachment.Guid != Guid.Empty)
{
Stream stream = await DataFileService.OpenFileStream(attachment.Guid);
return stream;
}
else
{
return null;
}
}
private Author GetOrCreateAuthor(object authorData, ChatItemConverterContext context)
{
Author author;
if (!this.authorDict.TryGetValue(authorData, out author))
{
ChatWithAttachmentsViewModel vm = (ChatWithAttachmentsViewModel)context.Chat.BindingContext;
if (object.Equals(vm.Me, authorData))
{
author = context.Chat.Author;
}
else
{
author = new Author();
author.Data = authorData;
author.Name = "" + authorData;
author.Avatar = string.Format("{0}.png", authorData);
}
this.authorDict[authorData] = author;
}
return author;
}
}
- The demo uses a custom data file server for uploading, downloading and deleting attachments:
c#
public static class DataFileService
{
internal static List<PredefinedFile> predefinedFiles;
private static readonly Dictionary<Guid, ServerFile> files = new Dictionary<Guid, ServerFile>();
internal static async Task Init()
{
if (predefinedFiles != null)
{
return;
}
List<PredefinedFile> list = new List<PredefinedFile>();
List<string> fileNames = new List<string>
{
"PdfDocument.pdf",
"Presentation.pptx",
"Accounting.xlsx",
"Audio.mp3",
"Video.mp4",
"PdfDocument-Signed.pdf",
"Archive.zip",
"TextFile.txt",
};
foreach (string fileName in fileNames)
{
using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes($"this is fake content for file {fileName}")))
{
Guid guid = await UploadFile(stream);
list.Add(new PredefinedFile { fileName = fileName, fileSize = stream.Length, guid = guid, });
}
}
predefinedFiles = list;
}
public static async Task<Guid> UploadFile(Stream stream)
{
await Task.Yield();
MemoryStream streamCopy = new MemoryStream();
stream.CopyTo(streamCopy);
ServerFile file = new ServerFile { stream = streamCopy };
lock (files)
{
Guid guid = Guid.NewGuid();
files[guid] = file;
return guid;
}
}
public static void DeleteFile(Guid guid)
{
ServerFile file;
lock (files)
{
if (!files.ContainsKey(guid))
{
return;
}
file = files[guid];
files.Remove(guid);
}
_ = Task.Run(() =>
{
lock (file)
{
file.stream.Dispose();
file.isDisposed = true;
}
});
}
public static async Task<Stream> OpenFileStream(Guid guid)
{
await Task.Yield();
ServerFile file;
lock (files)
{
file = files[guid];
}
MemoryStream streamCopy = new MemoryStream();
lock (file)
{
if (file.isDisposed)
{
throw new ObjectDisposedException("The file has been deleted from the server.");
}
file.stream.Position = 0;
file.stream.CopyTo(streamCopy);
streamCopy.Position = 0;
}
return streamCopy;
}
class ServerFile
{
internal MemoryStream stream;
internal bool isDisposed;
}
internal class PredefinedFile
{
internal string fileName;
internal long fileSize;
internal Guid guid;
}
}
