Search code examples
c#mefsystem.reactive

Making MEF plugins work with Rx


So the current situation is I have a program that is completely utilizing MEF. Now I want to make it utilize Rx as to allow it to scale to larger queries and allow the user to look over results as the various plugins return results. It is currently setup as such:

Workflow: Query => DetermineTypes => QueryPlugins => Results

Currently the code is all stored on GitHub if anyone needs to reference more than what I post below. ALeRT on GitHub

With the help of @Enigmativity the primary portion is now running with Rx. Now when I got that done, I thought I had the Framework setup to handle System.IObservable<string> by utilizing var.ToObservable(). This sadly doesn't seem to work (at least not as I have it). The plugin framework is currently setup as such:

public interface IQueryPlugin
{
    string PluginCategory { get; }
    string Name { get; }
    string Version { get; }
    string Author { get; }
    System.Collections.Generic.List<string> TypesAccepted { get; }
    System.IObservable<string> Result(string input, string type, bool sensitive);
}

One example plugin Result method that I was trying to fix, but failed looks as such:

public System.IObservable<string> Result(string input, string type, bool sensitive)
{

    string csv = "\"Roundtrip Time\"," + "\"Status\"\n";

    if (sensitive == true)
    {
        csv += "\"" + "" + "\"," + "\"" + "FORBIDDEN" + "\"\n";
    }
    else
    {
        if (type == "URL")
        {
            input = new Uri(input).Host;
        }
        Ping ping = new Ping();
        PingReply pingReply = ping.Send(input);

        csv += "\"" + pingReply.RoundtripTime.ToString() + "\"," + "\"" + pingReply.Status.ToString() + "\"\n";
    }

    return csv.ToObservable();
}

This naturally provides the following error: Cannot implicitly convert type System.IObservable<char> to System.IObservable<string>.

So the question is what is the best way to pass the data from a plugin to the main program. I can handle switching types if it benefits the situation and keeps the plugin interface relatively simple. The goal is to keep the plugin as simple as possible for any users who write their own.

And for a point of completion, I'll drop the whole MainWindow.xaml.cs below to see how it is all currently setup.

using ALeRT.PluginFramework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using GenericParsing;
using System.Windows.Markup;
using System.Data;
using System.Reactive.Linq;

namespace ALeRT.UI
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            var catalog = new AggregateCatalog();
            catalog.Catalogs.Add(new DirectoryCatalog(Directory.GetCurrentDirectory()));
            var container = new CompositionContainer(catalog);

            try
            {
                container.ComposeParts(this);
            }
            catch (CompositionException compositionException)
            {
                MessageBox.Show(compositionException.ToString());
            }

            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString());
            }
        }

        private void queryButton_Click(object sender, RoutedEventArgs e)
        {
            string line;

            //resultDS.Reset(); //Looking for a way to clear our the contents from last time without breaking SelectionChanged

            if (File.Exists(queryTB.Text) && (bool)listCB.IsChecked)
            {
                StreamReader file = null;
                try
                {
                    file = new StreamReader(queryTB.Text);
                    while ((line = file.ReadLine()) != null)
                    {
                        QueryPlugins(line, DetermineTypes(line), (bool)sensitiveCB.IsChecked);
                    }
                }
                finally
                {
                    if (file != null) { file.Close(); }
                }
            }
            else
            {
                QueryPlugins(queryTB.Text, DetermineTypes(queryTB.Text), (bool)sensitiveCB.IsChecked);
            }
        }

        DataSet resultsDS = new DataSet("Results");

        [ImportMany]
        public IEnumerable<ITypePlugin> TPlugins { get; set; }

        [ImportMany]
        public IEnumerable<IQueryPlugin> QPlugins { get; set; }

        /// <summary>
        /// Method to process all Type plugins.
        /// </summary>
        private List<string> DetermineTypes(string val)
        {
            List<string> typeResultAL = new List<string>();

            foreach (var tPlugins in this.TPlugins)
            {
                if (tPlugins.Result(val))
                {
                    typeResultAL.Add(tPlugins.Name);
                }
            }
            return typeResultAL;
        }

        /// <summary>
        /// Method to process all Query plugins.
        /// </summary>
        private void QueryPlugins(string query, List<string> types, bool sensitive)
        {
            foreach (string tType in types) //Cycle through a List<string>
            {
                foreach (var qPlugins in this.QPlugins) //Cycle through all query plugins
                {
                    foreach (string qType in qPlugins.TypesAccepted)  //Cycle though a List<string> within the IQueryPlugin interface AcceptedTypes
                    {
                        if (qType == tType) //Match the two List<strings>, one is the AcceptedTypes and the other is the one returned from ITypeQuery
                        {
                            IObservable<DataTable> q =
                                from text in qPlugins.Result(query, qType, sensitive)
                                from tempTable in Observable.Using(
                                () => new GenericParserAdapter(),
                                parser => Observable.Using(() => new StringReader(text),
                                    sr => Observable.Start<DataTable>(
                                        () =>
                                        {
                                            var rNum = new Random();
                                            parser.SetDataSource(sr);
                                            parser.ColumnDelimiter = Convert.ToChar(",");
                                            parser.FirstRowHasHeader = true;
                                            parser.MaxBufferSize = 4096;
                                            parser.MaxRows = 500;
                                            parser.TextQualifier = '\"';

                                            var tempTable = parser.GetDataTable();
                                            tempTable.TableName = qPlugins.Name.ToString();
                                            if (!tempTable.Columns.Contains("Query"))
                                            {
                                                DataColumn tColumn = new DataColumn("Query");
                                                tempTable.Columns.Add(tColumn);
                                                tColumn.SetOrdinal(0);
                                            }

                                            foreach (DataRow dr in tempTable.Rows)
                                                dr["Query"] = query;

                                            return tempTable;
                                        }
                                        )))
                                select tempTable;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Open a dialog prompt to select a file to process.
        /// </summary>
        private void browseButton_Click(object sender, RoutedEventArgs e)
        {
            Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();
            dlg.Filter = "All Files|*.*";

            Nullable<bool> result = dlg.ShowDialog();

            if (result == true)
            {
                queryTB.Text = dlg.FileName;
            }
        }

        private void pluginsLB_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            resultsDG.ItemsSource = resultsDS.Tables[pluginsLB.SelectedValue.ToString()].DefaultView;
        }
    }
}

Solution

  • Here's how I would write the Result method:

    public System.IObservable<string> Result(
        string input, string type, bool sensitive)
    {
        return Observable.Start(() =>
        {
            var csv = "\"Roundtrip Time\",\"Status\"\n";
    
            if (sensitive == true)
            {
                csv += "\"\",\"FORBIDDEN\"\n";
            }
            else
            {
                var input2 = type == "URL" ? new Uri(input).Host : input;
                ver ping = new Ping();
                ver pingReply = ping.Send(input2);
    
                csv += String.Format("\"{0}\",\"{1}\"\n",
                    pingReply.RoundtripTime, pingReply.Status);
            }
    
            return csv;
        });
    }
    

    The best way to write code for your plugin with Rx is to make sure that you stay within the observables for as much as possible - make your data into observables early in their life and only come out of observables as late in their life as possible. Then everything should fit together nicely.

    Does this help?