Search code examples
c#uwpuwp-xamlwinuiwinui-3

WinUI 3 UWP TabView NOT displaying New Tab when a new Item is added to the bound ItemsSource


I am using WinUI 3 UWP TabView in my App. I know that WinUI 3 is still in Preview stage for UWP. But still I want to know a workaround for my issue as I want to use TabView in my App. I have gone through the Official Documentation and GitHub Samples but I couldn't find a solution. The TabView is NOT displaying a New Tab whenever a New Document is added to the Collection. I have searched a lot but couldn't find a solution. Kindly, share a solution/workaround. You might suggest using WinUI 2 since it is stable for UWP. But, I have already tried that and WinUI 2 controls are not blending well with existing UWP Controls. But WinUI 3 blends perfectly. All other controls except TabView are working well. When I switch from DataBinding to Manually maintaining a list of TabItems, it works perfectly. But, I don't want Boilerplate code. I want to achieve the same with DataBinding. I am new to MVVM. So, if there's a problem with my ViewModel, do share a workaround.

This is my ViewModel Class:

    using Microsoft.UI.Xaml.Controls;
    using System.ComponentModel;
    using System.IO;
    using System.Text;
    using MyApp.Utilities;
    using System.Runtime.CompilerServices;
    namespace MyApp.ViewModels
     {
        public class TextDocument : INotifyPropertyChanged
      {
        private int _documentId;
        private string _fileName;
        private string _filePath;
        private string _textContent;
        private Encoding _encoding;
        private LineEnding _lineEnding;
        private bool _isSaved;
        public int DocumentId
        {
            get
            {
                return _documentId;
            }
            set
            {
                _documentId = value;
                OnPropertyChanged(ref _documentId, value);
            }
        }
        public string FileName
        {
            get
            {
                return _fileName;
            }
            set
            {
                OnPropertyChanged(ref _fileName, value);
            }
        }

        public string FilePath
        {
            get
            {
                return _filePath;
            }
            set
            {
                OnPropertyChanged(ref _filePath, value);
                FileName = Path.GetFileName(_filePath);
            }
        }

        public string TextContent
        {
            get
            {
                return _textContent;
            }
            set
            {
                OnPropertyChanged(ref _textContent, value);
            }
        }

        public Encoding FileEncoding
        {
            get
            {
                return _encoding;
            }
            set
            {
                OnPropertyChanged(ref _encoding, value);
            }
        }
        public LineEnding LineEnding
        {
            get
            {
                return _lineEnding;
            }
            set
            {
                OnPropertyChanged(ref _lineEnding, value);
            }
        }
        public string TabHeader
        {
            get
            {
               return string.IsNullOrEmpty(FileName) ? "Untitled Document" : FileName;
            }
        }
        public bool IsSaved
        {
            get
            {
                return _isSaved;
            }
            set
            {
                OnPropertyChanged(ref _isSaved, value);
            }
        }
        public bool IsInvalidFile 
        { 
            get 
            { 
                return (string.IsNullOrEmpty(FilePath) || string.IsNullOrEmpty(FileName)); 
            } 
        }
        public override bool Equals(object obj)
        {
            if (ReferenceEquals(obj, null))
                return false;
            if (ReferenceEquals(this, obj))
                return true;
            var comp = (TextDocument)obj;
            return this.DocumentId == comp.DocumentId;
        }
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged<T>(ref T property, T value, [CallerMemberName] string propertyName = "")
        {
            property = value;
            var handler = PropertyChanged;
            if (handler != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
 }

And this is my XAML Code for TabView:

<TabView x:Name="MyTabView" AddTabButtonClick="TabView_AddTabButtonClick" TabCloseRequested="TabView_TabCloseRequested"
             SelectionChanged="TabView_SelectionChanged"
             Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2" Background="White"
             HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
             TabItemsChanged="TabView_TabItemsChanged"
             SelectedIndex="0"
             TabItemsSource="{x:Bind MyDocuments,Mode=OneWay}"
             >
        <TabView.TabItemTemplate>
            <DataTemplate x:DataType="viewmodels:TextDocument">
                <TabViewItem Header="{x:Bind TabHeader,Mode=OneWay}" IconSource="{x:Null}">
                    <TabViewItem.Content>
                        <TextBox x:Name="TextBoxInsideTab" Grid.Column="0" Grid.Row="0" 
                                PlaceholderText="Drag and drop a file here or start typing"        
                                Text="{x:Bind TextContent,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" FontSize="24" 
                                UseSystemFocusVisuals="False"
                                BorderThickness="0"
                                VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
                                TextWrapping="Wrap"
                                IsSpellCheckEnabled="False"
                                CanBeScrollAnchor="True"
                                TextChanged="TextBox_TextChanged"
                                AcceptsReturn="True"
                                IsTabStop="True" 
                                ScrollViewer.VerticalScrollBarVisibility="Auto"
                                ScrollViewer.HorizontalScrollBarVisibility="Auto" 
                                />
                    </TabViewItem.Content>
                </TabViewItem>
            </DataTemplate>
        </TabView.TabItemTemplate>
    </TabView>

And this is my C# code:

    using Microsoft.UI.Xaml;
    using Microsoft.UI.Xaml.Controls;
    using Microsoft.UI.Xaml.Media;
    using MyApp.ViewModels;
    using Windows.Storage.Pickers;
    using Windows.Storage;
    using System;
    using System.Linq;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    namespace MyApp
{
    public sealed partial class MainPage : Page
    {
        private ObservableCollection<TextDocument> MyDocuments;
        public MainPage()
        {
            this.InitializeComponent();
            MyDocuments = new ObservableCollection<TextDocument>()
            {
                new TextDocument()
            };
        }
        private void TabView_AddTabButtonClick(TabView sender, object args)
        {
            AddTabViewItemDefault(sender.TabItems.Count);
        }
        private void AddTabViewItemDefault(int index)
        {
            MyDocuments.Add(new TextDocument());
        }
        private void TabView_TabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args)
        {
            MyDocuments.Remove(args.Item as TextDocument);
        }
        private void TabView_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
           
        }
        private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
        {

        }

    }
}

Solution

  • ObservableCollection<T> and INotifyCollectionChanged currently don't work in UWP apps.

    You need to implement your own custom collection as a workaround:

    using Microsoft.UI.Xaml.Data;
    using Microsoft.UI.Xaml.Interop;
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    
    using NotifyCollectionChangedAction = Microsoft.UI.Xaml.Interop.NotifyCollectionChangedAction;
    
    public class CustomObservableCollection<T> : Collection<T>, Microsoft.UI.Xaml.Interop.INotifyCollectionChanged, INotifyPropertyChanged
    {
        private ReentrancyGuard reentrancyGuard = null;
    
        private class ReentrancyGuard : IDisposable
        {
            private CustomObservableCollection<T> owningCollection;
    
            public ReentrancyGuard(CustomObservableCollection<T> owningCollection)
            {
                owningCollection.CheckReentrancy();
                owningCollection.reentrancyGuard = this;
                this.owningCollection = owningCollection;
            }
    
            public void Dispose()
            {
                owningCollection.reentrancyGuard = null;
            }
        }
    
        public CustomObservableCollection() : base() { }
        public CustomObservableCollection(IList<T> list) : base(list.ToList()) { }
        public CustomObservableCollection(IEnumerable<T> collection) : base(collection.ToList()) { }
    
        public event Microsoft.UI.Xaml.Interop.NotifyCollectionChangedEventHandler CollectionChanged;
    
        public void Move(int oldIndex, int newIndex)
        {
            MoveItem(oldIndex, newIndex);
        }
    
        protected IDisposable BlockReentrancy()
        {
            return new ReentrancyGuard(this);
        }
    
        protected void CheckReentrancy()
        {
            if (reentrancyGuard != null)
            {
                throw new InvalidOperationException("Collection cannot be modified in a collection changed handler.");
            }
        }
    
        protected override void ClearItems()
        {
            CheckReentrancy();
    
            TestBindableVector<T> oldItems = new TestBindableVector<T>(this);
    
            base.ClearItems();
            OnCollectionChanged(
                NotifyCollectionChangedAction.Reset,
                null, oldItems, 0, 0);
        }
    
        protected override void InsertItem(int index, T item)
        {
            CheckReentrancy();
    
            TestBindableVector<T> newItem = new TestBindableVector<T>();
            newItem.Add(item);
    
            base.InsertItem(index, item);
            OnCollectionChanged(
                NotifyCollectionChangedAction.Add,
                newItem, null, index, 0);
        }
    
        protected virtual void MoveItem(int oldIndex, int newIndex)
        {
            CheckReentrancy();
    
            TestBindableVector<T> oldItem = new TestBindableVector<T>();
            oldItem.Add(this[oldIndex]);
            TestBindableVector<T> newItem = new TestBindableVector<T>(oldItem);
    
            T item = this[oldIndex];
            base.RemoveAt(oldIndex);
            base.InsertItem(newIndex, item);
            OnCollectionChanged(
                NotifyCollectionChangedAction.Move,
                newItem, oldItem, newIndex, oldIndex);
        }
    
        protected override void RemoveItem(int index)
        {
            CheckReentrancy();
    
            TestBindableVector<T> oldItem = new TestBindableVector<T>();
            oldItem.Add(this[index]);
    
            base.RemoveItem(index);
            OnCollectionChanged(
                NotifyCollectionChangedAction.Remove,
                null, oldItem, 0, index);
        }
    
        protected override void SetItem(int index, T item)
        {
            CheckReentrancy();
    
            TestBindableVector<T> oldItem = new TestBindableVector<T>();
            oldItem.Add(this[index]);
            TestBindableVector<T> newItem = new TestBindableVector<T>();
            newItem.Add(item);
    
            base.SetItem(index, item);
            OnCollectionChanged(
                NotifyCollectionChangedAction.Replace,
                newItem, oldItem, index, index);
        }
    
        protected virtual void OnCollectionChanged(
            NotifyCollectionChangedAction action,
            IBindableVector newItems,
            IBindableVector oldItems,
            int newIndex,
            int oldIndex)
        {
            OnCollectionChanged(new Microsoft.UI.Xaml.Interop.NotifyCollectionChangedEventArgs(action, newItems, oldItems, newIndex, oldIndex));
        }
    
        protected virtual void OnCollectionChanged(Microsoft.UI.Xaml.Interop.NotifyCollectionChangedEventArgs e)
        {
            using (BlockReentrancy())
            {
                CollectionChanged?.Invoke(this, e);
            }
        }
    
    #pragma warning disable 0067 // PropertyChanged is never used, raising a warning, but it's needed to implement INotifyPropertyChanged.
        public event PropertyChangedEventHandler PropertyChanged;
    #pragma warning restore 0067
    }
    
    public class TestBindableVector<T> : IList<T>, IBindableVector
    {
        IList<T> implementation;
    
        public TestBindableVector() { implementation = new List<T>(); }
        public TestBindableVector(IList<T> list) { implementation = new List<T>(list); }
    
        public T this[int index] { get => implementation[index]; set => implementation[index] = value; }
    
        public int Count => implementation.Count;
    
        public virtual bool IsReadOnly => implementation.IsReadOnly;
    
        public void Add(T item)
        {
            implementation.Add(item);
        }
    
        public void Clear()
        {
            implementation.Clear();
        }
    
        public bool Contains(T item)
        {
            return implementation.Contains(item);
        }
    
        public void CopyTo(T[] array, int arrayIndex)
        {
            implementation.CopyTo(array, arrayIndex);
        }
    
        public IEnumerator<T> GetEnumerator()
        {
            return implementation.GetEnumerator();
        }
    
        public int IndexOf(T item)
        {
            return implementation.IndexOf(item);
        }
    
        public void Insert(int index, T item)
        {
            implementation.Insert(index, item);
        }
    
        public bool Remove(T item)
        {
            return implementation.Remove(item);
        }
    
        public void RemoveAt(int index)
        {
            implementation.RemoveAt(index);
        }
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            return implementation.GetEnumerator();
        }
    
        public object GetAt(uint index)
        {
            return implementation[(int)index];
        }
    
        public IBindableVectorView GetView()
        {
            return new TestBindableVectorView<T>(implementation);
        }
    
        public bool IndexOf(object value, out uint index)
        {
            int indexOf = implementation.IndexOf((T)value);
    
            if (indexOf >= 0)
            {
                index = (uint)indexOf;
                return true;
            }
            else
            {
                index = 0;
                return false;
            }
        }
    
        public void SetAt(uint index, object value)
        {
            implementation[(int)index] = (T)value;
        }
    
        public void InsertAt(uint index, object value)
        {
            implementation.Insert((int)index, (T)value);
        }
    
        public void RemoveAt(uint index)
        {
            implementation.RemoveAt((int)index);
        }
    
        public void Append(object value)
        {
            implementation.Add((T)value);
        }
    
        public void RemoveAtEnd()
        {
            implementation.RemoveAt(implementation.Count - 1);
        }
    
        public uint Size => (uint)implementation.Count;
    
        public IBindableIterator First()
        {
            return new TestBindableIterator<T>(implementation);
        }
    }
    
    public class TestBindableVectorView<T> : TestBindableVector<T>, IBindableVectorView
    {
        public TestBindableVectorView(IList<T> list) : base(list) { }
    
        public override bool IsReadOnly => true;
    }
    
    public class TestBindableIterator<T> : IBindableIterator
    {
        private readonly IEnumerator<T> enumerator;
    
        public TestBindableIterator(IEnumerable<T> enumerable) { enumerator = enumerable.GetEnumerator(); }
    
        public bool MoveNext()
        {
            return enumerator.MoveNext();
        }
    
        public object Current => enumerator.Current;
    
        public bool HasCurrent => enumerator.Current != null;
    }
    

    Page:

    public sealed partial class MainPage : Page
    {
        private CustomObservableCollection<TextDocument> MyDocuments;
        public MainPage()
        {
            this.InitializeComponent();
            MyDocuments = new CustomObservableCollection<TextDocument>()
            {
                new TextDocument()
            };
        }
        ...
    }