0

I am writing an application that updates files and then will eventually import the updated files to a database. I want to display a message about which file is being updated and when the process has finished. I want the message to come from the console because eventually the importer library I am using displays helpful messages through the console and I will want to display those too. I was able to do this before in a WPF app, but all of my code was in the code behind of the view, and I want to keep the MVVM pattern and separate the code into a ViewModel. My problem is I do not know how to get a reference to my TextBox that is in my View. Once I am able to get a hold of the TextBox in my ViewModel I will be able to send the Console Writes to the TextBox.

Here is my View

<Window x:Class="DICOM_Importer.Views.StudyImporterView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DICOM_Importer.Views"
        mc:Ignorable="d"
        Background="Gold"
        Title="Importer" Height="450" Width="800">
    <Grid Style="{StaticResource gridBackground}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="125" />
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="280" />
        </Grid.RowDefinitions>

        <StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Orientation="Horizontal">
            <Label Style="{Binding Source={StaticResource studyTitle}}" Content="Study:" />
            <Label Style="{Binding Source={StaticResource studyTitle}}" Name="StudyImportViewStudyText" Content="{Binding ImporterTitle}" />
        </StackPanel>

        <StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Orientation="Horizontal" >
            <Label Style="{Binding Source={StaticResource studyTitle}}" Content="Import Directory" />
            <Label Style="{Binding Source={StaticResource studyTitle}}" Content="{Binding ImporterPath}" />
        </StackPanel>

        <Button Grid.Column="2" Grid.Row="1" Command="{Binding ImportCommand}" Style="{Binding Source={StaticResource buttonStyleImport}}" Content="Submit" />

        <TextBox Grid.Column="0" Grid.ColumnSpan="3" Grid.Row="2" x:Name="ImportConsole" />

    </Grid>
</Window>

Here is the ViewModel

using DICOM_Importer.Commands;
using DICOM_Importer.Views;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;

namespace DICOM_Importer.ViewModels
{
    public class StudyImporterViewModel : INotifyPropertyChanged, IDataErrorInfo
    {
        private string importerTitle;
        private string importerPath;
        public DirectoryInfo[] directories;
        GetIndividualSubjectDirectories subjectDirectories = new GetIndividualSubjectDirectories();
        ConsoleOutputStream outputter;

        /// <summary>
        /// Gets the study information from the HomeView
        /// </summary>
        public String ImporterTitle
        {
            get { return importerTitle; }
            set
            {
                importerTitle = value;
                OnPropertyChanged("ImporterTitle");
            }
        }

        public String ImporterPath
        {
            get { return importerPath; }
            set
            {
                importerPath = value;
                OnPropertyChanged("ImporterPath");
            }
        }


        public StudyImporterViewModel()
        {
            ImportCommand = new ActivateImport(this);
            outputter = new ConsoleOutputStream(ImportConsole); //Here is where the error is
            Console.SetOut(outputter);
        }

        public ICommand ImportCommand
        {
            get;
            private set;
        }

        public void Import()
        {
            MessageBoxResult result = MessageBox.Show("This will import every series in the Import Directory. Are you sure you want to Import?", "Import Confirmation", MessageBoxButton.OKCancel);


            switch (result)
            {
                case MessageBoxResult.OK:
                    if(importerTitle == "SPIROMICS2")
                    {
                        Console.WriteLine("Importing SPIROMICS2 Scans to Mifar");
                        directories = subjectDirectories.GetSubjectDirectories(importerPath);
                        subjectDirectories.GetSeriesDirectories(directories);
                        Console.WriteLine("Import Complete");

                    }
                    else if(importerTitle == "BLF")
                    {
                        Console.WriteLine("BLF");
                    }
                    else if(importerTitle == "PRECISE")
                    {
                        Console.WriteLine("PRECISE");
                    }

                    break;
                case MessageBoxResult.Cancel:
                    MessageBox.Show("CANCEL", "Nope!");
                    break;
            }
        }

        #region Error Model
        public string Error
        {
            get;
            set;
        }
        #endregion

        #region Error Definition
        public string this[string columnName]
        {
            get 
            {
                if (columnName == "ImporterTitle")
                {
                    if (String.IsNullOrWhiteSpace(ImporterPath))
                    {
                        Error = "There is no selected study to import";
                    }
                    else 
                    {
                        Error = null;
                    }
                }
                return Error;
            }
        }
        #endregion

        #region PropertyChangedEventHandler
        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;

            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        #endregion


    }
}

Here is my ConsoleOutputStream Command

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;

namespace DICOM_Importer.Commands
{
    class ConsoleOutputStream : TextWriter
    {
        TextBox textBox = null;


        public ConsoleOutputStream(TextBox ouput)
        {
            textBox = ouput;
        }

        public override void Write(char value)
        {
            base.Write(value);
            textBox.Dispatcher.BeginInvoke(new Action(() => {
                textBox.AppendText(value.ToString());
            }));
        }

        public override Encoding Encoding
        {
            get { return System.Text.Encoding.UTF8; }
        }
    }
}

and here is the command for the button that will kick off all the file changes and imports

using DICOM_Importer.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace DICOM_Importer.Commands
{
    /// <summary>
    /// Starts the import processes on a button click if there is a study available 
    /// </summary>
    class ActivateImport : ICommand
    {

        private StudyImporterViewModel _studyImporterViewModel;

        public ActivateImport(StudyImporterViewModel viewModel)
        {
            _studyImporterViewModel = viewModel;
        }



        public event EventHandler CanExecuteChanged
        {
            //this is forcing the CommandManager to check the ICommand again. If we didn't have this then the buitton wouldl only be 
            //disabled if the window was loaded with a blank name, not if it was loaded with a name and then was deleted by the user
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter)
        {
            return String.IsNullOrWhiteSpace(_studyImporterViewModel.Error);
        }

        public void Execute(object parameter)
        {
            _studyImporterViewModel.Import();
        }
    }
}

Any help would be much appreciated!

3
  • You can't, in MVVM, get a reference to a view component from the viewmodel. That's breaking the pattern. Commented Mar 6, 2020 at 15:48
  • @sephiroth there is no way to bind the console write data from the ViewModel to the textbox in the View? Commented Mar 6, 2020 at 16:04
  • 1
    @millenniumThalken i think about a console app's output as a list of strings that we're fed one at a time. going this route, our view model would then have a List<string> property, maybe called ConsoleOutputs. so then our view would not have a TextBox, but maybe a ListBox, whose ItemSource is a Binding to our ConsoleOutputs. then, instead of using ConsoleOutputStream or even Console, we can just add new strings to ConsoleOutputs. when we add a new string to ConsoleOutputs, then through the magic of databinding our ListView will automaticallly be updated to display it. Commented Mar 6, 2020 at 16:33

1 Answer 1

3

You can redirect the Console output to a StringWriter and then write its content into the string you bound to the textbox every time a new console output is made.

ViewModel

private StringWriter _sw;
public string ConsoleOut { /* getter and setter */ }

// inside constructor
_sw = new StringWriter();
Console.SetOut(sw);
Console.SetError(sw);

xaml

<TextBlock Text="{Binding Path=ConsoleOut, Mode=OneWay}"/>

The problem here is that every time you want to display the console output in the TextBlock you need to update the value of S with S = _sw.ToString();


Solution

I found this answer with the implementation of an enhanced StringWriter class that fires an event every time there is a write. With this, you only need to make a simple update:

ViewModel

private StringWriterExt _sw;
public string ConsoleOut { /* getter and setter */ }

// inside constructor
_sw = new StringWriterExt(true);
Console.SetOut(sw);
Console.SetError(sw);
_sw.Flushed += (s, a) => ConsoleOut = _sw.ToString();
Sign up to request clarification or add additional context in comments.

2 Comments

I have been going over the input and link you provided but still feel like I a coding in circles. I am not sure how to go about making the ConsoleOut Model class that would contain the getter and setter.
public string ConsoleOut { get; set; } should work just fine with that event handler on _sw.Flushed

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.