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

        }

    }
}
10
  • 1
    Maybe add an UpdateSourceTrigger=PropertyChanged to TabItemsSource? Commented Aug 9, 2021 at 6:32
  • 1
    Is the initial TabItem displayed? Commented Aug 9, 2021 at 6: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. Commented Aug 9, 2021 at 6:38
  • 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... Commented Aug 9, 2021 at 7: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? Commented Aug 9, 2021 at 7:24

2 Answers 2

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()
        };
    }
    ...
}
Sign up to request clarification or add additional context in comments.

7 Comments

But ObservableCollection and TabView are working properly with WinUI 3 Desktop App. The problem exists only in WinUI 3 UWP Library it seems.
Severity Code Description Project File Line Suppression State Error CS1503 Argument 1: cannot convert from 'TypeX_Notepad_WinUI_3_UWP.CustomObservableCollection<TypeX_Notepad_WinUI_3_UWP.Models.TextDocument>' to 'System.Collections.ObjectModel.ObservableCollection<TypeX_Notepad_WinUI_3_UWP.Models.TextDocument>' 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
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); } } }
Yes, the issue only exists when using the UWP app model as the link I posted explains. Did you change to CustomObservableCollection<TextDocument> everywhere. It works for me.
I got an error in this.Update_MyDocuments() in MainPage.g.cs. If that error is removed somehow, everything will work finely.
|
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?

2 Comments

I will try this out and get back to you
Unfortunately this solution too doesn't solve the problem :-(

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.