2

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)
        {

        }

    }
}
Kris2k
  • 275
  • 2
  • 12
  • 1
    Maybe add an UpdateSourceTrigger=PropertyChanged to TabItemsSource? – Chris Aug 09 '21 at 06:32
  • 1
    Is the initial TabItem displayed? – Chris Aug 09 '21 at 06:35
  • @Chris Yes, the initial TabItem gets Displayed. But if AddTab Button is clicked, nothing shows up. But the item does get added to the ObservableCollection. And, I did try Mode=TwoWay,UpdateSourceTrigger=PropertyChanged. It doesn't work too. – Kris2k Aug 09 '21 at 06:38
  • @Chris I guess the new Tabs are overlapping the old Ones. Because, there is a change in ObservableCollection whenever a new Item is added or deleted. It is not showing up in the UI. Can you build the project with the code which I have sent and find a workaround for the problem ? – Kris2k Aug 09 '21 at 07:01
  • 1
    Did you see my answer below? Have you tried it? I´m sorry, I wont be able to build project, etc. I´m at work and busy doing other things... – Chris Aug 09 '21 at 07:13
  • 1
    Your ViewModel is fine. It must be something with the ObservableCollection not correctly binding to the TabControl. Maybe add INotifyPropertyChanged to the code behind class and notify ObservableCollection change after adding an item? – Chris Aug 09 '21 at 07:24
  • @Chris No problem. Thanks for helping out. I will try out your solution and will get back to you. – Kris2k Aug 09 '21 at 07:29
  • Did you try this: Maybe add INotifyPropertyChanged to the code behind class and notify ObservableCollection change after adding an item? – Chris Aug 09 '21 at 09:22
  • @Chris Yep, I tried out. It too didn't work – Kris2k Aug 09 '21 at 11:30
  • @Chris ObservableCollection and TabView are working properly with WinUI 3 Desktop App. The problem exists only in WinUI 3 UWP Library it seems. – Kris2k Aug 10 '21 at 03:31

2 Answers2

1

I think your code in the constructor might be breaking the initial binding to the ObservableCollection. Try this code:

private ObservableCollection<TextDocument> MyDocuments {get;} = new ObservableCollection<TextDocument>();

public MainPage()
{
    this.InitializeComponent();
    MyDocuments.Add(new TextDocument());
}

Does it help?

Chris
  • 835
  • 12
  • 27
1

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()
        };
    }
    ...
}
mm8
  • 163,881
  • 10
  • 57
  • 88
  • But ObservableCollection and TabView are working properly with WinUI 3 Desktop App. The problem exists only in WinUI 3 UWP Library it seems. – Kris2k Aug 10 '21 at 03:30
  • Severity Code Description Project File Line Suppression State Error CS1503 Argument 1: cannot convert from 'TypeX_Notepad_WinUI_3_UWP.CustomObservableCollection' to 'System.Collections.ObjectModel.ObservableCollection' TypeX Notepad WinUI 3 UWP C:\Users\arunp\source\repos\TypeX Notepad WinUI 3 UWP\TypeX Notepad WinUI 3 UWP\obj\x86\Debug\MainPage.g.cs 1024 Active – Kris2k Aug 10 '21 at 04:02
  • I am getting the following error in MainPage.g.cs while building the app. This is the method where the error occurs : private void Update_(global::TypeX_Notepad_WinUI_3_UWP.MainPage obj, int phase) { if (obj != null) { if ((phase & (NOT_PHASED | DATA_CHANGED | (1 << 0))) != 0) { this.Update_MyDocuments(obj.MyDocuments, phase); } } } – Kris2k Aug 10 '21 at 04:04
  • 1
    Yes, the issue only exists when using the UWP app model as the link I posted explains. Did you change to `CustomObservableCollection` everywhere. It works for me. – mm8 Aug 10 '21 at 13:39
  • I got an error in this.Update_MyDocuments() in MainPage.g.cs. If that error is removed somehow, everything will work finely. – Kris2k Aug 11 '21 at 03:07
  • The `MainPage.g.cs` class is generated based on your code. What error are you getting? You haven't even posted the implementation of `Update_MyDocuments()`. – mm8 Aug 11 '21 at 07:12
  • It is working now. Thanks a lot. The problem was with the IDE. When I changed the TabItemsSource from ObservableCollection to CustomObservableCollection, the Visual Studio IDE didn't change it accordingly in Main.g.cs. Removing TabItemsSource Binding line, Building the App and again adding TabItemsSource helped. – Kris2k Aug 12 '21 at 05:15