0

I am using a custom WindowsFormsHost control to wrap FastColoredTextbox winforms control in a tab control whose tabs are dynamically created when user hits the new document button via a DataTemplate.

Because of the limitations of the tab control which is explained in this SO question, my Undo/Redo logic was holding the history of all the tabs for each tab, which was resulting in wrong text being displayed.

So I decided to modify the FastColoredTextbox control to expose history and the redo stack, and added them to my custom WindowsFormsHost wrapper control as dependency properties which is bound two ways to my DocumentModel.

XAML:

<TabControl TabStripPlacement="Top" Margin="5" ItemsSource="{Binding Documents}" SelectedItem="{Binding CurrentDocument, Mode=TwoWay}" x:Name="TabDocuments">
    <TabControl.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="10" MaxWidth="10" MinWidth="10"/>
                    <ColumnDefinition Width="10" MaxWidth="10" MinWidth="10"/>
                </Grid.ColumnDefinitions>
                <TextBlock Grid.Column="0" Text="{Binding Metadata.FileName}"/>
                <TextBlock Grid.Column="1" Text="*" Visibility="{Binding IsSaved, Converter={StaticResource VisibilityConverter}}"/>
                <Button Grid.Column="2" Width="10" Height="10" MaxWidth="10" MaxHeight="10" MinWidth="10" MinHeight="10" VerticalAlignment="Center" HorizontalAlignment="Right" Command="{Binding CloseDocumentButtonCommand}">
                    <TextBlock Text="X" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="8" FontWeight="Bold"/>
                </Button>
            </Grid>
        </DataTemplate>
    </TabControl.ItemTemplate>
    <TabControl.ContentTemplate>
        <DataTemplate x:Name="TabDocumentsDataTemplate">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Border BorderBrush="Black" BorderThickness="1" Grid.Column="0" Margin="5">
                    <customControls:CodeTextboxHost Text="{Binding Markdown, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" History="{Binding History, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" RedoStack="{Binding RedoStack, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" WordWrap="True" x:Name="CodeTextboxHost"/>
                </Border>
                <Border BorderBrush="Black" BorderThickness="1" Grid.Column="1" Margin="5">
                    <wpf:ChromiumWebBrowser Address="{Binding Html, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" x:Name="ChromiumWebBrowser"/>    
                </Border>
            </Grid>
        </DataTemplate>
    </TabControl.ContentTemplate>
</TabControl>

The relevant part of the model:

public ObservableCollection<UndoableCommand> History
{
    get { return _history; }
    set
    {
        _history = value;
        OnPropertyChanged();
    }
}

public ObservableCollection<UndoableCommand> RedoStack
{
    get { return _redoStack; }
    set
    {
        _redoStack = value;
        OnPropertyChanged();
    }
}

The relevant parts of the custom control code:

public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(CodeTextboxHost), new PropertyMetadata("", new PropertyChangedCallback(
(d, e) =>
{
    var textBoxHost = d as CodeTextboxHost;
    if (textBoxHost != null && textBoxHost._innerTextbox != null)
    {
        textBoxHost._innerTextbox.Text = textBoxHost.GetValue(e.Property) as string;
    }
}), null));

public static readonly DependencyProperty HistoryProperty = DependencyProperty.Register("History", typeof(LimitedStack<UndoableCommand>), typeof(CodeTextboxHost), new PropertyMetadata(new LimitedStack<UndoableCommand>(200), new PropertyChangedCallback(
    (d, e) =>
    {
        var textBoxHost = d as CodeTextboxHost;
        if (textBoxHost != null && textBoxHost._innerTextbox != null)
        {
            var history = textBoxHost.GetValue(e.Property) as LimitedStack<UndoableCommand>;
            if (history != null)
            {
                textBoxHost._innerTextbox.TextSource.Manager.History = history;
                textBoxHost._innerTextbox.OnUndoRedoStateChanged();
            }
            else
            {
                textBoxHost._innerTextbox.ClearUndo();
            }
        }
    }), null));

public static readonly DependencyProperty RedoStackProperty = DependencyProperty.Register("RedoStack", typeof(Stack<UndoableCommand>), typeof(CodeTextboxHost), new PropertyMetadata(new Stack<UndoableCommand>(), new PropertyChangedCallback(
    (d, e) =>
    {
        var textBoxHost = d as CodeTextboxHost;
        if (textBoxHost != null && textBoxHost._innerTextbox != null)
        {
            var redoStack = textBoxHost.GetValue(e.Property) as Stack<UndoableCommand>;
            if (redoStack != null)
            {
                textBoxHost._innerTextbox.TextSource.Manager.RedoStack = redoStack;
                textBoxHost._innerTextbox.OnUndoRedoStateChanged();
            }
            else
            {
                textBoxHost._innerTextbox.ClearUndo();
            }
        }
    }), null));

public LimitedStack<UndoableCommand> History
{
    get { return (LimitedStack<UndoableCommand>) GetValue(HistoryProperty);}
    set { SetValue(HistoryProperty, value);}
}

public Stack<UndoableCommand> RedoStack
{
    get { return (Stack<UndoableCommand>) GetValue(RedoStackProperty); }
    set { SetValue(RedoStackProperty, value);}
}

public CodeTextboxHost()
{
    Child = _innerTextbox;
    _innerTextbox.Language = FastColoredTextBoxNS.Language.Custom;
    _innerTextbox.DescriptionFile = AppDomain.CurrentDomain.BaseDirectory + "SyntaxConfig\\MarkdownSyntaxHighlighting.xml";
    _innerTextbox.HighlightingRangeType = HighlightingRangeType.AllTextRange;
    _innerTextbox.TextChanged += _innerTextbox_TextChanged;
}

private void _innerTextbox_TextChanged(object sender, TextChangedEventArgs e)
{
    Text = _innerTextbox.Text;
    History = _innerTextbox.TextSource.Manager.History;
    RedoStack = _innerTextbox.TextSource.Manager.RedoStack; 
}

My problem is when TextChanged event is triggered, History or RedoStack does not update on the model while Text perfectly updates.

I tried adding RelativeSource as this SO question suggests but it didn't work.

Any help/ideas would be appreciated. Thank you.

Edit 1:

As suggested I made all collections ObservableCollection, however it didn't make any change, as the model is again not updated.

public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(CodeTextboxHost), new PropertyMetadata("", new PropertyChangedCallback(
    (d, e) =>
    {
        var textBoxHost = d as CodeTextboxHost;
        if (textBoxHost != null && textBoxHost._innerTextbox != null)
        {
            textBoxHost._innerTextbox.Text = textBoxHost.GetValue(e.Property) as string;
        }
    }), null));

public static readonly DependencyProperty HistoryProperty = DependencyProperty.Register("History", typeof(ObservableCollection<UndoableCommand>), typeof(CodeTextboxHost), new PropertyMetadata(new ObservableCollection<UndoableCommand>(), new PropertyChangedCallback(
    (d, e) =>
    {
        var textBoxHost = d as CodeTextboxHost;
        if (textBoxHost != null && textBoxHost._innerTextbox != null)
        {
            var history = textBoxHost.GetValue(e.Property) as ObservableCollection<UndoableCommand>;
            if (history != null)
            {
                textBoxHost._innerTextbox.TextSource.Manager.History = history.ToLimitedStack(200);
                textBoxHost._innerTextbox.OnUndoRedoStateChanged();
            }
            else
            {
                textBoxHost._innerTextbox.ClearUndo();
            }
        }
    }), null));

public static readonly DependencyProperty RedoStackProperty = DependencyProperty.Register("RedoStack", typeof(ObservableCollection<UndoableCommand>), typeof(CodeTextboxHost), new PropertyMetadata(new ObservableCollection<UndoableCommand>(), new PropertyChangedCallback(
    (d, e) =>
    {
        var textBoxHost = d as CodeTextboxHost;
        if (textBoxHost != null && textBoxHost._innerTextbox != null)
        {
            var redoStack = textBoxHost.GetValue(e.Property) as ObservableCollection<UndoableCommand>;
            if (redoStack != null)
            {
                textBoxHost._innerTextbox.TextSource.Manager.RedoStack = redoStack.ToStack();
                textBoxHost._innerTextbox.OnUndoRedoStateChanged();
            }
            else
            {
                textBoxHost._innerTextbox.ClearUndo();
            }
        }
    }), null));

public ObservableCollection<UndoableCommand> History
{
    get { return (ObservableCollection<UndoableCommand>) GetValue(HistoryProperty);}
    set { SetValue(HistoryProperty, value);}
}

public ObservableCollection<UndoableCommand> RedoStack
{
    get { return (ObservableCollection<UndoableCommand>) GetValue(RedoStackProperty); }
    set { SetValue(RedoStackProperty, value);}
}

public string Text
{
    get { return (string)GetValue(TextProperty); }
    set { SetValue(TextProperty, value); }
}

public bool WordWrap
{
    get { return (bool)GetValue(WordWrapProperty); }
    set { SetValue(WordWrapProperty, value); }
}

public CodeTextboxHost()
{
    Child = _innerTextbox;
    _innerTextbox.Language = FastColoredTextBoxNS.Language.Custom;
    _innerTextbox.DescriptionFile = AppDomain.CurrentDomain.BaseDirectory + "SyntaxConfig\\MarkdownSyntaxHighlighting.xml";
    _innerTextbox.HighlightingRangeType = HighlightingRangeType.AllTextRange;
    _innerTextbox.TextChanged += _innerTextbox_TextChanged;
}

private void _innerTextbox_TextChanged(object sender, TextChangedEventArgs e)
{
    Text = _innerTextbox.Text;
    History = _innerTextbox.TextSource.Manager.History.ToOveObservableCollection();
    RedoStack = _innerTextbox.TextSource.Manager.RedoStack.ToObservableCollection();
}
Murat Aykanat
  • 1,648
  • 4
  • 29
  • 38
  • If I had to guess, I would say the issue starts with the fact that you use `ObservableCollection` in the model, and `Stack` and `LimitedStack` in the dependency properties. I have only had good luck with binding collections when these match. I use `ObservableCollection` in all. – R. Richards Jul 02 '17 at 22:07
  • I cannot figure out the full code but there are work arounds in case you want to explicitly call updates. Try Interaction Trigger on TextBox Text Changed It will invoke an command with parameter then you can record the changes in you DS – Ramankingdom Jul 12 '17 at 06:24
  • @Ramankingdom I actually use that, the only difference is that the Textbox itself is a winforms control, and I wrap it by a `WindowsFormsHost`. I use the inner textbox's `TextChanged` event to update the text, history and redo stack (as can be seen in the `_innerTextbox_TextChanged` method). However the problem is only text updates, the rest does not update and stored back in the model. – Murat Aykanat Jul 12 '17 at 11:09
  • @maykanat Is the call is not propagating to the Code where the stack is updating ? – Ramankingdom Jul 14 '17 at 09:54
  • @Ramankingdom For example,I put a break point in the `DocumentModel` where the `DocumentText` property updates. When `TextChanged` is triggered, the `Text`property of the `CodeTextboxHost` updates, the break point is reached in the `DocumentModel` and the `DocumentText` property actually updates in the `DocumentModel`.However when I do the same for `RedoStack` and `History`, when `TextChanged` is triggered nothing happens on the Model side as if the UI didn't notify the `DocumentModel`. So the binding works from Model to UI but not the other way around for `ObservableCollections`. – Murat Aykanat Jul 14 '17 at 10:08

2 Answers2

0

I was able to make the UI change the Model by modifying the custom control code as follows:

public static readonly DependencyProperty HistoryProperty = DependencyProperty.Register("History", typeof(IEnumerable<UndoableCommand>), typeof(CodeTextboxHost), new FrameworkPropertyMetadata(new ObservableCollection<UndoableCommand>(), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(
    (d, e) =>
    {
        var textBoxHost = d as CodeTextboxHost;
        if (textBoxHost != null && textBoxHost._innerTextbox != null)
        {
            var history = textBoxHost.GetValue(e.Property) as ObservableCollection<UndoableCommand>;
            if (history != null)
            {
                textBoxHost._innerTextbox.TextSource.Manager.History = history.ToLimitedStack(200);
                textBoxHost._innerTextbox.OnUndoRedoStateChanged();
            }
            else
            {
                textBoxHost._innerTextbox.ClearUndo();
            }
        }
    }), null));

public static readonly DependencyProperty RedoStackProperty = DependencyProperty.Register("RedoStack", typeof(IEnumerable<UndoableCommand>), typeof(CodeTextboxHost), new FrameworkPropertyMetadata(new ObservableCollection<UndoableCommand>(), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(
    (d, e) =>
    {
        var textBoxHost = d as CodeTextboxHost;
        if (textBoxHost != null && textBoxHost._innerTextbox != null)
        {
            var redoStack = textBoxHost.GetValue(e.Property) as ObservableCollection<UndoableCommand>;
            if (redoStack != null)
            {
                textBoxHost._innerTextbox.TextSource.Manager.RedoStack = redoStack.ToStack();
                textBoxHost._innerTextbox.OnUndoRedoStateChanged();
            }
            else
            {
                textBoxHost._innerTextbox.ClearUndo();
            }
        }
    }), null));

public ObservableCollection<UndoableCommand> History
{
    get { return (ObservableCollection<UndoableCommand>) GetValue(HistoryProperty);}
    set { SetCurrentValue(HistoryProperty, new ObservableCollection<UndoableCommand>(value));}
}

public ObservableCollection<UndoableCommand> RedoStack
{
    get { return (ObservableCollection<UndoableCommand>) GetValue(RedoStackProperty); }
    set { SetCurrentValue(RedoStackProperty, new ObservableCollection<UndoableCommand>(value));}
}

So instead of SetValue, I used SetCurrentValue on History and RedoStack to make the UI update the model correctly.

Murat Aykanat
  • 1,648
  • 4
  • 29
  • 38
0

use INotifyPropertyChanged interface and implement OnPropertyChanged method and put that method where do you want to update the interface

kalsara Magamage
  • 263
  • 2
  • 16
  • Maybe a link to a better answer would help : https://stackoverflow.com/questions/7934236/inotifypropertychanged-and-observablecollection-wpf – Digvijay Jul 13 '17 at 15:02
  • I don't want to update the UI, the current data binding works from the model to the UI fine. The problem is, when I do the update in UI in the observable collections, it doesn't update the model's observable collections. – Murat Aykanat Jul 14 '17 at 09:50
  • Use two way binding on your mode of the xaml. – kalsara Magamage Jul 17 '17 at 09:07