Skip to main content
deleted 3877 characters in body; edited title
Source Link
Jamal
  • 35.2k
  • 13
  • 134
  • 238

How do I make this code unit Unit-testable?testing the importing of data into a database

Below is the almost complete code (stripped a couple DAL calls and anonymized company-specifics) for one of those functionalities. I'm not going to supply DAL and Presentation layer code here, since my concerns are mainly around the Business Logic layer - but for the record I have DAL implemented with Linq to SQL and Presentation taken care of with WPF (which I just started learning). So here it goes:

  • If I wanted to write unit tests for this functionality, what would I need to do in order to make the code unit-testable? I have no issues making methods public, as they wouldn't be exposed to COM anyway (because only IFunctionality is accessible through COM). Or perhaps I should make them protected, and derive from this class so I could test it (the test wrapper class would expose public methods that call into the protected ones; tests would call those exposed public methods and it could even have dependencies injected into its constructor - am I thinking the right way about this?).
  • If I wanted to write unit tests for this functionality, I couldn't write a test for the Execute method, right? I'd have to test the more specialized code and figure out how to mock the DAL calls? And for CanExecute, I'd need a way to get rid of the CurrentUser dependency and inject some dummy provider that doesn't hit the database nor Active Directory, right?
  • Is this code "clean"? (easy to read, easy to understand, easy to maintain) I guess the sole fact that I'm asking this, answers for itself... How could it be made cleaner then?
  • Are static classes and methods hindering anything? (like CurrentUser.IsAuthorized, FileDialogHelper.GetOpenFileName, and Excel8OleDbHelper.ImportExcelFile) - MsgBox lives in the Presentation layer and it aims at replacing the ugly default message box, I think the static class is warranted in that particular case.
  • Sharp eyes will have noticed I initiate a db transaction, do some work while showing progress (the process can take anywhere between a minute and 2 hours, depending on inputs) and then committing or rolling back the transaction only after the ProgressMsgBox has closed. This leaves the affected tables unnecessarily locked, longer than they need to be. How can I work around this? (hmm now that I'm thinking about it, I could have the ViewModel raise an event when progress completes and handle it in this class to commit or rollback as needed... but I'd have to make data a property of the class... makes sense? Then I guess the functionality class would need to implement IDisposable and rely on the client code to properly dispose its resources, no?)

EDIT

Following up with the answer I got, here's the refactored class - there's still a parameterless constructor for easy COM-client instantiation, but there's also one that allows unit tests to inject mock-up dependencies:

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IFunctionality))]
public class ImportFunctionality : FunctionalityBase
{
    private IImporter _importer;

    /// <summary>
    /// Instantiates a new <see cref="ImportFunctionality"/> with default dependencies.
    /// </summary>
    public ImportFunctionality()
        : this(new CurrentUser(), new Importer(
                                    new ExcelSourceProvider(new FileDialogHelper(), new Excel8OleDbHelper()), 
                                    new ImportSettingsProvider(new DataModel(Settings.Default.DefaultDb)))
              ) { }

    /// <summary>
    /// Instantiates a new <see cref="ImportFunctionality"/> with specified dependencies.
    /// </summary>
    /// <param name="user"></param>
    /// <param name="importer"></param>
    public ImportFunctionality(ICurrentUser user, IImporter importer)
        : base(user) 
    {
        _importer = importer;
    }

    public override void Execute()
    {
        _importer.DoImport();
    }
}

And the new base functionality class, bound to be massively reused:

public abstract class FunctionalityBase : IFunctionality
{
    protected ICurrentUser CurrentUser { get; private set; }

    public FunctionalityBase(ICurrentUser user) 
    {
        CurrentUser = user;
    }

    public abstract void Execute();

    public virtual bool CanExecute()
    {
        return CurrentUser.IsAuthorized((this as IFunctionality).AuthId);
    }

    void IFunctionality.Execute()
    {
        if ((this as IFunctionality).CanExecute())
        { 
            Execute(); 
        }
        else
        {
            throw new NotAuthorizedException(resx.Functionality_Execute_NotAuthorised); 
        }
    }

    bool IFunctionality.CanExecute()
    {
        return CanExecute();
    }

    string IFunctionality.AuthId
    {
        get { return GetType().ToString(); }
    }
}

The default implementation for IImporter is where the UI-dependent code now resides:

/// <summary>
/// Encapsulates business logic for the importing functionality.
/// </summary>
public class Importer : IImporter
{
    private IExcelSourceProvider _excelDataSourceProvider;
    private IImportSettingsProvider _importSettingsProvider;

    public Importer(IExcelSourceProvider dataSourceProvider, IImportSettingsProvider settingsProvider)
    {
        _excelDataSourceProvider = dataSourceProvider;
        _importSettingsProvider = settingsProvider;
    }

    public void DoImport()
    {
        var xlData = _excelDataSourceProvider.GetExcelSourceData();
        if (xlData == null) return;

        var viewModel = _importSettingsProvider.GetImportSettings(xlData);
        if (viewModel == null) return;

        if (!GetUserConfirmation(viewModel)) return;

        ImportGridContent(viewModel);
    }
    ...

There's still a little bit of refactoring left to be done in the default IImporter implementation (some more dependencies to be injected rather than instantiated), but overall I think this is exactly where I need to be, and continuing down this road will lead to easily readable, maintainable and testable code. Thanks!

  1. If I wanted to write unit tests for this functionality, what would I need to do in order to make the code unit-testable? I have no issues making methods public, as they wouldn't be exposed to COM anyway (because only IFunctionality is accessible through COM). Or perhaps I should make them protected, and derive from this class so I could test it (the test wrapper class would expose public methods that call into the protected ones; tests would call those exposed public methods and it could even have dependencies injected into its constructor - am I thinking the right way about this?).
  2. If I wanted to write unit tests for this functionality, I couldn't write a test for the Execute method, right? I'd have to test the more specialized code and figure out how to mock the DAL calls? And for CanExecute, I'd need a way to get rid of the CurrentUser dependency and inject some dummy provider that doesn't hit the database nor Active Directory, right?
  3. Is this code "clean"? (easy to read, easy to understand, easy to maintain) I guess the sole fact that I'm asking this, answers for itself... How could it be made cleaner then?
  4. Are static classes and methods hindering anything? (like CurrentUser.IsAuthorized, FileDialogHelper.GetOpenFileName, and Excel8OleDbHelper.ImportExcelFile) - MsgBox lives in the Presentation layer and it aims at replacing the ugly default message box, I think the static class is warranted in that particular case.
  5. Sharp eyes will have noticed I initiate a db transaction, do some work while showing progress (the process can take anywhere between a minute and 2 hours, depending on inputs) and then committing or rolling back the transaction only after the ProgressMsgBox has closed. This leaves the affected tables unnecessarily locked, longer than they need to be. How can I work around this? (hmm now that I'm thinking about it, I could have the ViewModel raise an event when progress completes and handle it in this class to commit or rollback as needed... but I'd have to make data a property of the class... makes sense? Then I guess the functionality class would need to implement IDisposable and rely on the client code to properly dispose its resources, no?)

How do I make this code unit-testable?

Below is the almost complete code (stripped a couple DAL calls and anonymized company-specifics) for one of those functionalities. I'm not going to supply DAL and Presentation layer code here, since my concerns are mainly around the Business Logic layer - but for the record I have DAL implemented with Linq to SQL and Presentation taken care of with WPF (which I just started learning). So here it goes:

  • If I wanted to write unit tests for this functionality, what would I need to do in order to make the code unit-testable? I have no issues making methods public, as they wouldn't be exposed to COM anyway (because only IFunctionality is accessible through COM). Or perhaps I should make them protected, and derive from this class so I could test it (the test wrapper class would expose public methods that call into the protected ones; tests would call those exposed public methods and it could even have dependencies injected into its constructor - am I thinking the right way about this?).
  • If I wanted to write unit tests for this functionality, I couldn't write a test for the Execute method, right? I'd have to test the more specialized code and figure out how to mock the DAL calls? And for CanExecute, I'd need a way to get rid of the CurrentUser dependency and inject some dummy provider that doesn't hit the database nor Active Directory, right?
  • Is this code "clean"? (easy to read, easy to understand, easy to maintain) I guess the sole fact that I'm asking this, answers for itself... How could it be made cleaner then?
  • Are static classes and methods hindering anything? (like CurrentUser.IsAuthorized, FileDialogHelper.GetOpenFileName, and Excel8OleDbHelper.ImportExcelFile) - MsgBox lives in the Presentation layer and it aims at replacing the ugly default message box, I think the static class is warranted in that particular case.
  • Sharp eyes will have noticed I initiate a db transaction, do some work while showing progress (the process can take anywhere between a minute and 2 hours, depending on inputs) and then committing or rolling back the transaction only after the ProgressMsgBox has closed. This leaves the affected tables unnecessarily locked, longer than they need to be. How can I work around this? (hmm now that I'm thinking about it, I could have the ViewModel raise an event when progress completes and handle it in this class to commit or rollback as needed... but I'd have to make data a property of the class... makes sense? Then I guess the functionality class would need to implement IDisposable and rely on the client code to properly dispose its resources, no?)

EDIT

Following up with the answer I got, here's the refactored class - there's still a parameterless constructor for easy COM-client instantiation, but there's also one that allows unit tests to inject mock-up dependencies:

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IFunctionality))]
public class ImportFunctionality : FunctionalityBase
{
    private IImporter _importer;

    /// <summary>
    /// Instantiates a new <see cref="ImportFunctionality"/> with default dependencies.
    /// </summary>
    public ImportFunctionality()
        : this(new CurrentUser(), new Importer(
                                    new ExcelSourceProvider(new FileDialogHelper(), new Excel8OleDbHelper()), 
                                    new ImportSettingsProvider(new DataModel(Settings.Default.DefaultDb)))
              ) { }

    /// <summary>
    /// Instantiates a new <see cref="ImportFunctionality"/> with specified dependencies.
    /// </summary>
    /// <param name="user"></param>
    /// <param name="importer"></param>
    public ImportFunctionality(ICurrentUser user, IImporter importer)
        : base(user) 
    {
        _importer = importer;
    }

    public override void Execute()
    {
        _importer.DoImport();
    }
}

And the new base functionality class, bound to be massively reused:

public abstract class FunctionalityBase : IFunctionality
{
    protected ICurrentUser CurrentUser { get; private set; }

    public FunctionalityBase(ICurrentUser user) 
    {
        CurrentUser = user;
    }

    public abstract void Execute();

    public virtual bool CanExecute()
    {
        return CurrentUser.IsAuthorized((this as IFunctionality).AuthId);
    }

    void IFunctionality.Execute()
    {
        if ((this as IFunctionality).CanExecute())
        { 
            Execute(); 
        }
        else
        {
            throw new NotAuthorizedException(resx.Functionality_Execute_NotAuthorised); 
        }
    }

    bool IFunctionality.CanExecute()
    {
        return CanExecute();
    }

    string IFunctionality.AuthId
    {
        get { return GetType().ToString(); }
    }
}

The default implementation for IImporter is where the UI-dependent code now resides:

/// <summary>
/// Encapsulates business logic for the importing functionality.
/// </summary>
public class Importer : IImporter
{
    private IExcelSourceProvider _excelDataSourceProvider;
    private IImportSettingsProvider _importSettingsProvider;

    public Importer(IExcelSourceProvider dataSourceProvider, IImportSettingsProvider settingsProvider)
    {
        _excelDataSourceProvider = dataSourceProvider;
        _importSettingsProvider = settingsProvider;
    }

    public void DoImport()
    {
        var xlData = _excelDataSourceProvider.GetExcelSourceData();
        if (xlData == null) return;

        var viewModel = _importSettingsProvider.GetImportSettings(xlData);
        if (viewModel == null) return;

        if (!GetUserConfirmation(viewModel)) return;

        ImportGridContent(viewModel);
    }
    ...

There's still a little bit of refactoring left to be done in the default IImporter implementation (some more dependencies to be injected rather than instantiated), but overall I think this is exactly where I need to be, and continuing down this road will lead to easily readable, maintainable and testable code. Thanks!

Unit-testing the importing of data into a database

Below is the almost complete code (stripped a couple DAL calls and anonymized company-specifics) for one of those functionalities. I'm not going to supply DAL and Presentation layer code here, since my concerns are mainly around the Business Logic layer - but for the record I have DAL implemented with Linq to SQL and Presentation taken care of with WPF (which I just started learning).

  1. If I wanted to write unit tests for this functionality, what would I need to do in order to make the code unit-testable? I have no issues making methods public, as they wouldn't be exposed to COM anyway (because only IFunctionality is accessible through COM). Or perhaps I should make them protected, and derive from this class so I could test it (the test wrapper class would expose public methods that call into the protected ones; tests would call those exposed public methods and it could even have dependencies injected into its constructor - am I thinking the right way about this?).
  2. If I wanted to write unit tests for this functionality, I couldn't write a test for the Execute method, right? I'd have to test the more specialized code and figure out how to mock the DAL calls? And for CanExecute, I'd need a way to get rid of the CurrentUser dependency and inject some dummy provider that doesn't hit the database nor Active Directory, right?
  3. Is this code "clean"? (easy to read, easy to understand, easy to maintain) I guess the sole fact that I'm asking this, answers for itself... How could it be made cleaner then?
  4. Are static classes and methods hindering anything? (like CurrentUser.IsAuthorized, FileDialogHelper.GetOpenFileName, and Excel8OleDbHelper.ImportExcelFile) - MsgBox lives in the Presentation layer and it aims at replacing the ugly default message box, I think the static class is warranted in that particular case.
  5. Sharp eyes will have noticed I initiate a db transaction, do some work while showing progress (the process can take anywhere between a minute and 2 hours, depending on inputs) and then committing or rolling back the transaction only after the ProgressMsgBox has closed. This leaves the affected tables unnecessarily locked, longer than they need to be. How can I work around this? (hmm now that I'm thinking about it, I could have the ViewModel raise an event when progress completes and handle it in this class to commit or rollback as needed... but I'd have to make data a property of the class... makes sense? Then I guess the functionality class would need to implement IDisposable and rely on the client code to properly dispose its resources, no?)
Updated with refactored code
Source Link
Mathieu Guindon
  • 75.6k
  • 18
  • 195
  • 469

EDIT

Following up with the answer I got, here's the refactored class - there's still a parameterless constructor for easy COM-client instantiation, but there's also one that allows unit tests to inject mock-up dependencies:

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IFunctionality))]
public class ImportFunctionality : FunctionalityBase
{
    private IImporter _importer;

    /// <summary>
    /// Instantiates a new <see cref="ImportFunctionality"/> with default dependencies.
    /// </summary>
    public ImportFunctionality()
        : this(new CurrentUser(), new Importer(
                                    new ExcelSourceProvider(new FileDialogHelper(), new Excel8OleDbHelper()), 
                                    new ImportSettingsProvider(new DataModel(Settings.Default.DefaultDb)))
              ) { }

    /// <summary>
    /// Instantiates a new <see cref="ImportFunctionality"/> with specified dependencies.
    /// </summary>
    /// <param name="user"></param>
    /// <param name="importer"></param>
    public ImportFunctionality(ICurrentUser user, IImporter importer)
        : base(user) 
    {
        _importer = importer;
    }

    public override void Execute()
    {
        _importer.DoImport();
    }
}

And the new base functionality class, bound to be massively reused:

public abstract class FunctionalityBase : IFunctionality
{
    protected ICurrentUser CurrentUser { get; private set; }

    public FunctionalityBase(ICurrentUser user) 
    {
        CurrentUser = user;
    }

    public abstract void Execute();

    public virtual bool CanExecute()
    {
        return CurrentUser.IsAuthorized((this as IFunctionality).AuthId);
    }

    void IFunctionality.Execute()
    {
        if ((this as IFunctionality).CanExecute())
        { 
            Execute(); 
        }
        else
        {
            throw new NotAuthorizedException(resx.Functionality_Execute_NotAuthorised); 
        }
    }

    bool IFunctionality.CanExecute()
    {
        return CanExecute();
    }

    string IFunctionality.AuthId
    {
        get { return GetType().ToString(); }
    }
}

The default implementation for IImporter is where the UI-dependent code now resides:

/// <summary>
/// Encapsulates business logic for the importing functionality.
/// </summary>
public class Importer : IImporter
{
    private IExcelSourceProvider _excelDataSourceProvider;
    private IImportSettingsProvider _importSettingsProvider;

    public Importer(IExcelSourceProvider dataSourceProvider, IImportSettingsProvider settingsProvider)
    {
        _excelDataSourceProvider = dataSourceProvider;
        _importSettingsProvider = settingsProvider;
    }

    public void DoImport()
    {
        var xlData = _excelDataSourceProvider.GetExcelSourceData();
        if (xlData == null) return;

        var viewModel = _importSettingsProvider.GetImportSettings(xlData);
        if (viewModel == null) return;

        if (!GetUserConfirmation(viewModel)) return;

        ImportGridContent(viewModel);
    }
    ...

There's still a little bit of refactoring left to be done in the default IImporter implementation (some more dependencies to be injected rather than instantiated), but overall I think this is exactly where I need to be, and continuing down this road will lead to easily readable, maintainable and testable code. Thanks!

EDIT

Following up with the answer I got, here's the refactored class - there's still a parameterless constructor for easy COM-client instantiation, but there's also one that allows unit tests to inject mock-up dependencies:

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IFunctionality))]
public class ImportFunctionality : FunctionalityBase
{
    private IImporter _importer;

    /// <summary>
    /// Instantiates a new <see cref="ImportFunctionality"/> with default dependencies.
    /// </summary>
    public ImportFunctionality()
        : this(new CurrentUser(), new Importer(
                                    new ExcelSourceProvider(new FileDialogHelper(), new Excel8OleDbHelper()), 
                                    new ImportSettingsProvider(new DataModel(Settings.Default.DefaultDb)))
              ) { }

    /// <summary>
    /// Instantiates a new <see cref="ImportFunctionality"/> with specified dependencies.
    /// </summary>
    /// <param name="user"></param>
    /// <param name="importer"></param>
    public ImportFunctionality(ICurrentUser user, IImporter importer)
        : base(user) 
    {
        _importer = importer;
    }

    public override void Execute()
    {
        _importer.DoImport();
    }
}

And the new base functionality class, bound to be massively reused:

public abstract class FunctionalityBase : IFunctionality
{
    protected ICurrentUser CurrentUser { get; private set; }

    public FunctionalityBase(ICurrentUser user) 
    {
        CurrentUser = user;
    }

    public abstract void Execute();

    public virtual bool CanExecute()
    {
        return CurrentUser.IsAuthorized((this as IFunctionality).AuthId);
    }

    void IFunctionality.Execute()
    {
        if ((this as IFunctionality).CanExecute())
        { 
            Execute(); 
        }
        else
        {
            throw new NotAuthorizedException(resx.Functionality_Execute_NotAuthorised); 
        }
    }

    bool IFunctionality.CanExecute()
    {
        return CanExecute();
    }

    string IFunctionality.AuthId
    {
        get { return GetType().ToString(); }
    }
}

The default implementation for IImporter is where the UI-dependent code now resides:

/// <summary>
/// Encapsulates business logic for the importing functionality.
/// </summary>
public class Importer : IImporter
{
    private IExcelSourceProvider _excelDataSourceProvider;
    private IImportSettingsProvider _importSettingsProvider;

    public Importer(IExcelSourceProvider dataSourceProvider, IImportSettingsProvider settingsProvider)
    {
        _excelDataSourceProvider = dataSourceProvider;
        _importSettingsProvider = settingsProvider;
    }

    public void DoImport()
    {
        var xlData = _excelDataSourceProvider.GetExcelSourceData();
        if (xlData == null) return;

        var viewModel = _importSettingsProvider.GetImportSettings(xlData);
        if (viewModel == null) return;

        if (!GetUserConfirmation(viewModel)) return;

        ImportGridContent(viewModel);
    }
    ...

There's still a little bit of refactoring left to be done in the default IImporter implementation (some more dependencies to be injected rather than instantiated), but overall I think this is exactly where I need to be, and continuing down this road will lead to easily readable, maintainable and testable code. Thanks!

Tweeted twitter.com/#!/StackCodeReview/status/321939329859612672
deleted 5216 characters in body
Source Link
Mathieu Guindon
  • 75.6k
  • 18
  • 195
  • 469
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IFunctionality))]
public class ImportFunctionality : IFunctionality
{
    private ImportWindow _window; // a WPF view
    
    public ImportFunctionality()
    { }

    public bool CanExecute()
    {
        return CurrentUser.IsAuthorized(AuthId);
    }

    public string AuthId
    {
        get { return GetType().ToString(); }
    }

    /// <summary>
    /// Prompts for an Excel workbook filename and creates pricing tables from workbook data.
    /// </summary>
    public void Execute()
    {
        try
        {
            if (!CanExecute()) throw new NotAuthorizedException(resx.Functionality_Execute_NotAuthorised);

            var xlData = GetExcelSourceData();
            if (xlData == null) return;
    
            var viewModel = GetImportSettings(xlData);
            if (viewModel == null) return;

            if (!GetUserConfirmation(viewModel)) return;
            ImportGridContent(viewModel);
        }
        catch (NotAuthorizedException exception)
        {
            MsgBox.Failure(resx.NotAuthorized, exception.Message);
        }
        catch (Exception exception)
        {
            MsgBox.Error(exception);
        }
    }

    private DataTable GetExcelSourceData()
    {
        var file = FileDialogHelper.GetOpenFileName(resx.FileDialogFilter_Excel97_2003);
        if (file == string.Empty) return null;

        return Excel8OleDbHelper.ImportExcelFile(file);
    }

    private ImportViewModel GetImportSettings(DataTable xlData)
    {
        var viewModel = new ImportViewModel(xlData);

        // todo: add metadata to viewModel constructor...
        using (var data = new ImportModel(Settings.Default.DefaultDb))
        {
            // Wrap Linq2SQL objects into UI-visible instances:
            var meta = data.LoadImportMetadata()
                            .Select(e => new ImportMetaData
                                            {
                                                Id = e.Id,
                                                TableSuffix = e.TableSuffix,
                                                TableName = e.TableName,
                                                Operator = e.Operator == null
                                                    ? ImportColumnOperator.None 
                                                    : (ImportColumnOperator)Enum.Parse(typeof(ImportColumnOperator), e.Operator),
                                               // OperandImportColumnNamenamed =property e.OperandImportColumnNamesetters
                                            }).ToList();

            // load metadata into ViewModel:
            viewModel.SetMetadata(new ObservableCollection<ImportMetaData>(meta));
        }

        _window = new ImportWindow(viewModel);

        viewModel.WindowClosing += viewModel_WindowClosing;
        _window.ShowDialog();

        var result = viewModel.DialogResult;
        return (result == DialogResult.Ok)
                        ? viewModel
                        : null;
    }

    private bool GetUserConfirmation(ImportViewModel viewModel)
    {
        var msg = string.Format(resx.ImportFunctionality_ConfirmProceed, viewModel.SelectedCompany, viewModel.SelectedPriceList, viewModel.SelectedDatabase);
        var result = MsgBox.Prompt(resx.ConfirmationRequired, msgresx.ImportFunctionality_ConfirmProceed);
        return (result == DialogResult.Yes);
    }

    void viewModel_WindowClosing(object sender, EventArgs e)
    {
        _window.Close();
    }

    /// <summary>
    /// Returns a <see cref="IImportData"/> implementation that corresponds to database name specified by supplied <see cref="viewModel"/>.
    /// </summary>
    /// <param name="viewModel"></param>
    /// <returns></returns>
    public IImportData GetTargetConnection(ImportViewModel viewModel)
    {
        var connectionString = Settings.Default[viewModel.SelectedDatabase].ToString();
        return viewModel.SelectedCompany == CompaniesEnum.Company1.ToString()
                                            ? new ImportDataCompany1(connectionString)
                                            : viewModel.SelectedCompany == CompaniesEnum.Company2.ToString()
                                                ? new ImportDataCompany2(connectionString)
                                                : new ImportData(connectionString)
                                                // this is begging to be refactored
                                                ;
    }

    private void ImportGridContent(ImportViewModel viewModel)
    {
        using (var data = GetTargetConnection(viewModel))
        {
            var args = new AsyncImportEventArgs(viewModel, data);
            var iterations = viewModel.GridSource.Rows.Count * viewModel.SelectedMetadata.Count();

            data.BeginTransaction();
            MsgBox.ProgressStatus<AsyncImportEventArgs>(resx.TariffImportFunctionality_TitleImportFunctionality_Title, resx.TariffImportFunctionality_PleaseWaitImportFunctionality_PleaseWait, DoAsyncImport, args, CancelAsyncImport, iterations);

            if (args.Success)
                data.CommitTransaction();
            else
                data.RollbackTransaction();
        }
    }

    protected void DoAsyncImport(AsyncProgressArgs<AsyncImportEventArgs> e)
    {
        // number of iterations in update batch (progress bar gets updated when progress reaches this value):
        const int iterationsToUpdate = 10;
        const string codeColumnName = "Code";
        const string dimensionColumnName = "Size";

        var data = e.Arguments.Data;
        var viewModel = e.Arguments.ViewModel;

        e.Arguments.Success = false;
        e.AppendMessageDetails(string.Format("{0,-20}\tInsertions\r\n", "Code"));
        e.AppendMessageDetails(new string('-', 36) + "\r\n");

        var importCodes = viewModel.GridSource.Rows.Cast<DataRow>()
                                    .GroupBy(r => new { Code = r[codeColumnName].ToString() })
                                    .Select(g => g.Key.Code);

        var totalRowInserts = 0;
        foreach (var importCode in importCodes)
        {
            foreach (var meta in viewModel.SelectedMetadata)
            {
                // check if user has requested to cancel theLengthy operation:
                e.CheckCancelled();

                var itemCode = string.Format("{0}-{1}", importCode, meta.TableSuffix);
                e.UpdateProgress(string.Format(resx.ImportFunctionality_PreparingItemData, itemCode));

                data.WipeExistingData(itemCode, viewModel.SelectedPriceList);
           involving DAL calls and periodically data.InsertDimensions(itemCode,updating newthe SizeF());
view
                var suffixItemCount = 0; // counter for processed rows under current meta
                var progress = 0; // counter for current progress update batch

                var filteredRows = viewModel.GridSource.Rows.Cast<DataRow>()
                                    .Where(r => r[codeColumnName].ToString() == importCode);

                var dimensions = filteredRows
                                    .GroupBy(r => new { Size = BusinessMathHelper.ConvertInchesToMilimeters(r[dimensionColumnName].ToString()) })
                                    .OrderBy(r => r.Key.Size.Height)
                                    .ThenBy(r => r.Key.Size.Width);

                var heights = dimensions.Select(g => g.Key.Size.Height).Distinct().OrderBy(h => h);
                var widths = dimensions.Select(g => g.Key.Size.Width).Distinct().OrderBy(w => w);

                foreach (var h in heights) data.InsertDimensionRows(itemCode, h);
                foreach (var w in widths) data.InsertDimensionColumns(itemCode, w);

                foreach (var row in dimensions.SelectMany(g => g.Select(r => r)))
                {
                    if (row[dimensionColumnName] != null)
                    {
                        var size = BusinessMathHelper.ConvertInchesToMilimeters(row[dimensionColumnName].ToString());
                        var value = CalculateRowValue(row, meta);

                        data.InsertContent(itemCode, viewModel.SelectedPriceList, size, value);
                        progress++; suffixItemCount++; totalRowInserts++;
                        if (progress == iterationsToUpdate)
                        {
                                e.IncrementProgress(string.Format(resx.ImportFunctionality_ImportingGridData, itemCode, suffixItemCount / Convert.ToDouble(filteredRows.Count())), progress);
                            progress = 0;
                        }
                    }
                }

                e.UpdateProgress(string.Format(resx.ImportFunctionality_CreatingPriceTable, itemCode));
                CreatePriceTable(data, viewModel, itemCode, importCode);

                e.AppendMessageDetails(string.Format("{0,-20}\t{1,5:n0}\r\n", itemCode, suffixItemCount));
                e.Arguments.Success = true;
            }
        }

        // Update the view:
        e.UpdateProgress(resx.ProgressCompleted, totalRowInsertsmaxProgressValue);
        e.UpdateMessage(resx.ImportFunctionality_TitleCompleted);

        e.AppendMessageDetails(new string('-', 36) + "\r\n");
        e.AppendMessageDetails(string.Format("{0,-20}\t{1,5:n0}", "Total", totalRowInserts));
    }

    private void CreatePriceTable(IImportData data, ImportViewModel viewModel, string itemCode, string importCode)
    {
        //data.Insert...
    }

    private void CancelAsyncImport(AsyncImportEventArgs e)
    {
        e.Success = false;
        e.Data.RollbackTransaction();
        MsgBox.Info(resx.ImportFunctionality_Cancelled_Title, resx.ImportFunctionality_Cancelled_Msg);
    }

    private int CalculateRowValue(DataRow row, ImportMetaData meta)
    {
        var value = Convert.ToInt32(row[meta.TableName] == DBNull.Value ? 0 : row[meta.TableName]);
        switch (meta.Operator)
        {
            case ImportColumnOperator.Plus:
                value += Convert.ToInt32(row[meta.OperandImportColumnName] == DBNull.Value ? 0 : row[meta.OperandImportColumnName]);
                break;

            case ImportColumnOperator.Minus:
                value -= Convert.ToInt32(row[meta.OperandImportColumnName] == DBNull.Value ? 0 : row[meta.OperandImportColumnName]);
                break;

            case ImportColumnOperator.Multiply:
                value *= Convert.ToInt32(row[meta.OperandImportColumnName] == DBNull.Value ? 0 : row[meta.OperandImportColumnName]);
                break;

            case ImportColumnOperator.Negate:
                value *= -1;
                break;
        }

        return value;
    }

    #region nested types

    /// <summary>
    /// Encapsulates parameters passed to async method for importing pricing tables.
    /// </summary>
    public class AsyncImportEventArgs : EventArgs
    {
        public ImportViewModel ViewModel { get; private set; }
        public IImportData Data { get; private set; }
        public bool Success { get; set; }

        public AsyncImportEventArgs(ImportViewModel dialog, IImportData data)
        {
            ViewModel = dialog;
            Data = data;
        }
    }

    #endregion
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IFunctionality))]
public class ImportFunctionality : IFunctionality
{
    private ImportWindow _window; // a WPF view
    
    public ImportFunctionality()
    { }

    public bool CanExecute()
    {
        return CurrentUser.IsAuthorized(AuthId);
    }

    public string AuthId
    {
        get { return GetType().ToString(); }
    }

    /// <summary>
    /// Prompts for an Excel workbook filename and creates pricing tables from workbook data.
    /// </summary>
    public void Execute()
    {
        try
        {
            if (!CanExecute()) throw new NotAuthorizedException(resx.Functionality_Execute_NotAuthorised);

            var xlData = GetExcelSourceData();
            if (xlData == null) return;
    
            var viewModel = GetImportSettings(xlData);
            if (viewModel == null) return;

            if (!GetUserConfirmation(viewModel)) return;
            ImportGridContent(viewModel);
        }
        catch (NotAuthorizedException exception)
        {
            MsgBox.Failure(resx.NotAuthorized, exception.Message);
        }
        catch (Exception exception)
        {
            MsgBox.Error(exception);
        }
    }

    private DataTable GetExcelSourceData()
    {
        var file = FileDialogHelper.GetOpenFileName(resx.FileDialogFilter_Excel97_2003);
        if (file == string.Empty) return null;

        return Excel8OleDbHelper.ImportExcelFile(file);
    }

    private ImportViewModel GetImportSettings(DataTable xlData)
    {
        var viewModel = new ImportViewModel(xlData);

        // todo: add metadata to viewModel constructor...
        using (var data = new ImportModel(Settings.Default.DefaultDb))
        {
            // Wrap Linq2SQL objects into UI-visible instances:
            var meta = data.LoadImportMetadata()
                            .Select(e => new ImportMetaData
                                            {
                                                Id = e.Id,
                                                TableSuffix = e.TableSuffix,
                                                TableName = e.TableName,
                                                Operator = e.Operator == null
                                                    ? ImportColumnOperator.None 
                                                    : (ImportColumnOperator)Enum.Parse(typeof(ImportColumnOperator), e.Operator),
                                                OperandImportColumnName = e.OperandImportColumnName
                                            }).ToList();

            // load metadata into ViewModel:
            viewModel.SetMetadata(new ObservableCollection<ImportMetaData>(meta));
        }

        _window = new ImportWindow(viewModel);

        viewModel.WindowClosing += viewModel_WindowClosing;
        _window.ShowDialog();

        var result = viewModel.DialogResult;
        return (result == DialogResult.Ok)
                        ? viewModel
                        : null;
    }

    private bool GetUserConfirmation(ImportViewModel viewModel)
    {
        var msg = string.Format(resx.ImportFunctionality_ConfirmProceed, viewModel.SelectedCompany, viewModel.SelectedPriceList, viewModel.SelectedDatabase);
        var result = MsgBox.Prompt(resx.ConfirmationRequired, msg);
        return (result == DialogResult.Yes);
    }

    void viewModel_WindowClosing(object sender, EventArgs e)
    {
        _window.Close();
    }

    /// <summary>
    /// Returns a <see cref="IImportData"/> implementation that corresponds to database name specified by supplied <see cref="viewModel"/>.
    /// </summary>
    /// <param name="viewModel"></param>
    /// <returns></returns>
    public IImportData GetTargetConnection(ImportViewModel viewModel)
    {
        var connectionString = Settings.Default[viewModel.SelectedDatabase].ToString();
        return viewModel.SelectedCompany == CompaniesEnum.Company1.ToString()
                                            ? new ImportDataCompany1(connectionString)
                                            : viewModel.SelectedCompany == CompaniesEnum.Company2.ToString()
                                                ? new ImportDataCompany2(connectionString)
                                                : new ImportData(connectionString);
    }

    private void ImportGridContent(ImportViewModel viewModel)
    {
        using (var data = GetTargetConnection(viewModel))
        {
            var args = new AsyncImportEventArgs(viewModel, data);
            var iterations = viewModel.GridSource.Rows.Count * viewModel.SelectedMetadata.Count();

            data.BeginTransaction();
            MsgBox.ProgressStatus<AsyncImportEventArgs>(resx.TariffImportFunctionality_Title, resx.TariffImportFunctionality_PleaseWait, DoAsyncImport, args, CancelAsyncImport, iterations);

            if (args.Success)
                data.CommitTransaction();
            else
                data.RollbackTransaction();
        }
    }

    protected void DoAsyncImport(AsyncProgressArgs<AsyncImportEventArgs> e)
    {
        // number of iterations in update batch (progress bar gets updated when progress reaches this value):
        const int iterationsToUpdate = 10;
        const string codeColumnName = "Code";
        const string dimensionColumnName = "Size";

        var data = e.Arguments.Data;
        var viewModel = e.Arguments.ViewModel;

        e.Arguments.Success = false;
        e.AppendMessageDetails(string.Format("{0,-20}\tInsertions\r\n", "Code"));
        e.AppendMessageDetails(new string('-', 36) + "\r\n");

        var importCodes = viewModel.GridSource.Rows.Cast<DataRow>()
                                    .GroupBy(r => new { Code = r[codeColumnName].ToString() })
                                    .Select(g => g.Key.Code);

        var totalRowInserts = 0;
        foreach (var importCode in importCodes)
        {
            foreach (var meta in viewModel.SelectedMetadata)
            {
                // check if user has requested to cancel the operation:
                e.CheckCancelled();

                var itemCode = string.Format("{0}-{1}", importCode, meta.TableSuffix);
                e.UpdateProgress(string.Format(resx.ImportFunctionality_PreparingItemData, itemCode));

                data.WipeExistingData(itemCode, viewModel.SelectedPriceList);
                data.InsertDimensions(itemCode, new SizeF());

                var suffixItemCount = 0; // counter for processed rows under current meta
                var progress = 0; // counter for current progress update batch

                var filteredRows = viewModel.GridSource.Rows.Cast<DataRow>()
                                    .Where(r => r[codeColumnName].ToString() == importCode);

                var dimensions = filteredRows
                                    .GroupBy(r => new { Size = BusinessMathHelper.ConvertInchesToMilimeters(r[dimensionColumnName].ToString()) })
                                    .OrderBy(r => r.Key.Size.Height)
                                    .ThenBy(r => r.Key.Size.Width);

                var heights = dimensions.Select(g => g.Key.Size.Height).Distinct().OrderBy(h => h);
                var widths = dimensions.Select(g => g.Key.Size.Width).Distinct().OrderBy(w => w);

                foreach (var h in heights) data.InsertDimensionRows(itemCode, h);
                foreach (var w in widths) data.InsertDimensionColumns(itemCode, w);

                foreach (var row in dimensions.SelectMany(g => g.Select(r => r)))
                {
                    if (row[dimensionColumnName] != null)
                    {
                        var size = BusinessMathHelper.ConvertInchesToMilimeters(row[dimensionColumnName].ToString());
                        var value = CalculateRowValue(row, meta);

                        data.InsertContent(itemCode, viewModel.SelectedPriceList, size, value);
                        progress++; suffixItemCount++; totalRowInserts++;
                        if (progress == iterationsToUpdate)
                        {
                                e.IncrementProgress(string.Format(resx.ImportFunctionality_ImportingGridData, itemCode, suffixItemCount / Convert.ToDouble(filteredRows.Count())), progress);
                            progress = 0;
                        }
                    }
                }

                e.UpdateProgress(string.Format(resx.ImportFunctionality_CreatingPriceTable, itemCode));
                CreatePriceTable(data, viewModel, itemCode, importCode);

                e.AppendMessageDetails(string.Format("{0,-20}\t{1,5:n0}\r\n", itemCode, suffixItemCount));
                e.Arguments.Success = true;
            }
        }

        e.UpdateProgress(resx.ProgressCompleted, totalRowInserts);
        e.UpdateMessage(resx.ImportFunctionality_TitleCompleted);

        e.AppendMessageDetails(new string('-', 36) + "\r\n");
        e.AppendMessageDetails(string.Format("{0,-20}\t{1,5:n0}", "Total", totalRowInserts));
    }

    private void CreatePriceTable(IImportData data, ImportViewModel viewModel, string itemCode, string importCode)
    {
        //data.Insert...
    }

    private void CancelAsyncImport(AsyncImportEventArgs e)
    {
        e.Success = false;
        e.Data.RollbackTransaction();
        MsgBox.Info(resx.ImportFunctionality_Cancelled_Title, resx.ImportFunctionality_Cancelled_Msg);
    }

    private int CalculateRowValue(DataRow row, ImportMetaData meta)
    {
        var value = Convert.ToInt32(row[meta.TableName] == DBNull.Value ? 0 : row[meta.TableName]);
        switch (meta.Operator)
        {
            case ImportColumnOperator.Plus:
                value += Convert.ToInt32(row[meta.OperandImportColumnName] == DBNull.Value ? 0 : row[meta.OperandImportColumnName]);
                break;

            case ImportColumnOperator.Minus:
                value -= Convert.ToInt32(row[meta.OperandImportColumnName] == DBNull.Value ? 0 : row[meta.OperandImportColumnName]);
                break;

            case ImportColumnOperator.Multiply:
                value *= Convert.ToInt32(row[meta.OperandImportColumnName] == DBNull.Value ? 0 : row[meta.OperandImportColumnName]);
                break;

            case ImportColumnOperator.Negate:
                value *= -1;
                break;
        }

        return value;
    }

    #region nested types

    /// <summary>
    /// Encapsulates parameters passed to async method for importing pricing tables.
    /// </summary>
    public class AsyncImportEventArgs : EventArgs
    {
        public ImportViewModel ViewModel { get; private set; }
        public IImportData Data { get; private set; }
        public bool Success { get; set; }

        public AsyncImportEventArgs(ImportViewModel dialog, IImportData data)
        {
            ViewModel = dialog;
            Data = data;
        }
    }

    #endregion
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IFunctionality))]
public class ImportFunctionality : IFunctionality
{
    private ImportWindow _window; // a WPF view
    
    public ImportFunctionality()
    { }

    public bool CanExecute()
    {
        return CurrentUser.IsAuthorized(AuthId);
    }

    public string AuthId
    {
        get { return GetType().ToString(); }
    }

    /// <summary>
    /// Prompts for an Excel workbook filename and creates pricing tables from workbook data.
    /// </summary>
    public void Execute()
    {
        try
        {
            if (!CanExecute()) throw new NotAuthorizedException(resx.Functionality_Execute_NotAuthorised);

            var xlData = GetExcelSourceData();
            if (xlData == null) return;
    
            var viewModel = GetImportSettings(xlData);
            if (viewModel == null) return;

            if (!GetUserConfirmation(viewModel)) return;
            ImportGridContent(viewModel);
        }
        catch (NotAuthorizedException exception)
        {
            MsgBox.Failure(resx.NotAuthorized, exception.Message);
        }
        catch (Exception exception)
        {
            MsgBox.Error(exception);
        }
    }

    private DataTable GetExcelSourceData()
    {
        var file = FileDialogHelper.GetOpenFileName(resx.FileDialogFilter_Excel97_2003);
        if (file == string.Empty) return null;

        return Excel8OleDbHelper.ImportExcelFile(file);
    }

    private ImportViewModel GetImportSettings(DataTable xlData)
    {
        var viewModel = new ImportViewModel(xlData);

        // todo: add metadata to viewModel constructor...
        using (var data = new ImportModel(Settings.Default.DefaultDb))
        {
            // Wrap Linq2SQL objects into UI-visible instances:
            var meta = data.LoadImportMetadata()
                            .Select(e => new ImportMetaData
                                            {
                                                // named property setters
                                            }).ToList();

            // load metadata into ViewModel:
            viewModel.SetMetadata(new ObservableCollection<ImportMetaData>(meta));
        }

        _window = new ImportWindow(viewModel);

        viewModel.WindowClosing += viewModel_WindowClosing;
        _window.ShowDialog();

        var result = viewModel.DialogResult;
        return (result == DialogResult.Ok)
                        ? viewModel
                        : null;
    }

    private bool GetUserConfirmation(ImportViewModel viewModel)
    {
        var result = MsgBox.Prompt(resx.ConfirmationRequired, resx.ImportFunctionality_ConfirmProceed);
        return (result == DialogResult.Yes);
    }

    void viewModel_WindowClosing(object sender, EventArgs e)
    {
        _window.Close();
    }

    /// <summary>
    /// Returns a <see cref="IImportData"/> implementation that corresponds to database name specified by supplied <see cref="viewModel"/>.
    /// </summary>
    /// <param name="viewModel"></param>
    /// <returns></returns>
    public IImportData GetTargetConnection(ImportViewModel viewModel)
    {
        var connectionString = Settings.Default[viewModel.SelectedDatabase].ToString();
        return viewModel.SelectedCompany == CompaniesEnum.Company1.ToString()
                                            ? new ImportDataCompany1(connectionString)
                                            : viewModel.SelectedCompany == CompaniesEnum.Company2.ToString()
                                                ? new ImportDataCompany2(connectionString)
                                                : new ImportData(connectionString)
                                                // this is begging to be refactored
                                                ;
    }

    private void ImportGridContent(ImportViewModel viewModel)
    {
        using (var data = GetTargetConnection(viewModel))
        {
            var args = new AsyncImportEventArgs(viewModel, data);
            var iterations = viewModel.GridSource.Rows.Count * viewModel.SelectedMetadata.Count();

            data.BeginTransaction();
            MsgBox.ProgressStatus<AsyncImportEventArgs>(resx.ImportFunctionality_Title, resx.ImportFunctionality_PleaseWait, DoAsyncImport, args, CancelAsyncImport, iterations);

            if (args.Success)
                data.CommitTransaction();
            else
                data.RollbackTransaction();
        }
    }

    protected void DoAsyncImport(AsyncProgressArgs<AsyncImportEventArgs> e)
    {
        // Lengthy operation involving DAL calls and periodically updating the view
        // ...

        // Update the view:
        e.UpdateProgress(resx.ProgressCompleted, maxProgressValue);
        e.UpdateMessage(resx.ImportFunctionality_TitleCompleted);

    }

    private void CancelAsyncImport(AsyncImportEventArgs e)
    {
        e.Success = false;
        e.Data.RollbackTransaction();
        MsgBox.Info(resx.ImportFunctionality_Cancelled_Title, resx.ImportFunctionality_Cancelled_Msg);
    }

    #region nested types

    /// <summary>
    /// Encapsulates parameters passed to async method for importing pricing tables.
    /// </summary>
    public class AsyncImportEventArgs : EventArgs
    {
        public ImportViewModel ViewModel { get; private set; }
        public IImportData Data { get; private set; }
        public bool Success { get; set; }

        public AsyncImportEventArgs(ImportViewModel dialog, IImportData data)
        {
            ViewModel = dialog;
            Data = data;
        }
    }

    #endregion
}
Removed out-of-context rant about current legacy code... and reworded the title
Source Link
Mathieu Guindon
  • 75.6k
  • 18
  • 195
  • 469
Loading
Source Link
Mathieu Guindon
  • 75.6k
  • 18
  • 195
  • 469
Loading