This is a basically a class library project which is somehow exposed as a WCF service. The code below is a part of the Data Access Layer. 'db' is an object of a DataContext class. To save a file, we do the following-
public static Guid SaveFile(FileDetails fileDetails)
{
System.Nullable<Guid> id = null;
SystemDataContext.UsingWrite(db =>
{
db.SaveFileData(fileDetails.RunId, fileDetails.FileData, fileDetails.FileExtension, ref id);
});
return id ?? Guid.Empty;
}
Then, the below would execute-
public static void UsingWrite(Action<SoftCashCreditDBDataContext> action)
{
using (var context = new SystemDataContext())
{
try
{
action(context.Write);
}
catch (Exception ex)
{
DataAccessExceptionHandler.HandleExcetion(ex, Config.DataLayerPolicy);
}
}
}
public SystemDataContext()
{
if (_stack == null)
{
_stack = new Stack<SystemDataContext>();
this.Depth = 1;
this.Read = new SoftCashCreditDBDataContext(Config.ReadDatabaseConnection);
this.Write = new SoftCashCreditDBDataContext(Config.WriteDatabaseConnection);
}
else
{
var parent = _stack.Peek();
/// Increment level of node.
this.Depth = parent.Depth + 1;
/// Copy data context from the parent
this.Read = parent.Read;
this.Write = parent.Write;
}
_stack.Push(this);
}
public int Depth { get; private set; }
public bool IsRoot { get { return this.Depth == 1; } }
[ThreadStatic]
private static Stack<SystemDataContext> _stack = null;
public SoftCashCreditDBDataContext Read { get; private set; }
public SoftCashCreditDBDataContext Write { get; private set; }
#region IDisposable Members
public void Dispose()
{
var context = _stack.Pop();
if (context.IsRoot == true)
{
context.Read.Dispose();
context.Write.Dispose();
_stack = null;
}
}
#endregion
}
They have implemented LINQ to SQL here, and created a DBContext class. The 'SaveFileData()' method is actually part of that class, where it just calls an SP inside to save the file.
What I did not follow-
What exactly does the call to UsingWrite() do here? What is passed to the 'Action action' parameter, and what is it doing?
I understand your confusion. They use 2 delegates.
This is passed to the action parameter:
db =>
{
db.SaveFileData(fileDetails.RunId, fileDetails.FileData, fileDetails.FileExtension, ref id);
}
So when UsingWrite is called, the SoftCashCreditDBDataContext delegate which was set in the Write delegate will call SaveFileData.
A simplified example to help you understand Action:
public void Main()
{
Test(x => Debug.Write(x));
}
private void Test(Action<string> testAction)
{
testAction("Bla");
}
This function will call Debug.Write with the argument x, which is a string that is passed to the test action function.
Related
I'm using a unity container and I'm trying to resolve by passing the object to the parameterized constructor, I noticed the same constructor is called twice, the first time it takes appropriate values, and not sure why it is calling again and it overrides with a blank object, can someone help me what is happening over here, not able to solve it.
//////////////////////////////////////////////////////////////////////
if (container == null)
{
container = new UnityContainer().AddExtension(new Diagnostic());
container.RegisterType<ISubscribeService,OOrderProc.Common.SubscribeService.SubscribeService>();
container.RegisterType<IBaseOrderProcessing, BaseSubscribe>("Subscribe");
}
SubscribeDetails m = new SubscribeDetails();
m.SubscribeType = SubscribeType.ACTIVATE;
m.SubscribeName = "TEST";
var b = container.Resolve<IBaseOrderProcessing>("Subscribe",new DependencyOverride<BaseSubscribe>(new OOrderProc.Common.SubscribeService.SubscribeService(m)));
//////////////////////////////////////////////////////////////////////
public interface IBaseOrderProcessing
{
void ProcessOrder();
}
public interface ISubscribeService
{
SubscribeType SubscribeType { get; set; }
void ActivateSubscribe();
void UpgradeSubscribe();
}
// Strategy Pattern 1 => Subscribe is one of the "if" condition
public class BaseSubscribe : IBaseOrderProcessing
{
private ISubscribeService _SubscribeService = null;
public BaseSubscribe(ISubscribeService SubscribeService)
{
_SubscribeService = SubscribeService;
}
public void ProcessOrder()
{
if (_SubscribeService.SubscribeType == SubscribeType.ACTIVATE)
_SubscribeService.ActivateSubscription();
if (_SubscribeService.SubscribeType == SubscribeType.UPGRADE)
_SubscribeService.UpgradeSubscription();
}
}
// Writing another class to simplify is correct ?????
public class SubscribeService : ISubscribeService
{
private SubscribeDetails _Subscribedetails = null;
public SubscribeType SubscribeType { get; set; }
public SubscribeService(SubscribeDetails Subscribedetails)
{
_Subscribedetails = Subscribedetails;
SubscribeType = Subscribedetails.SubscribeType;
}
public void ActivateSubscription()
{
// Code to save the Subscribe details in the database
Console.WriteLine($"\n\nSubscribe {_Subscribedetails.SubscribeId} for {_Subscribedetails.SubscribeName} activated for order Id: {_Subscribedetails.OrderId}" +
$" from {_Subscribedetails.SubscribeStartDate} to {_Subscribedetails.SubscribeEndDate}");
}
public void UpgradeSubscription()
{
// Code to upgrade the Subscribe details in the database
Console.WriteLine($"\n\nSubscribe {_Subscribedetails.SubscribeId} for {_Subscribedetails.SubscribeName} upgraded for order Id: {_Subscribedetails.OrderId}" +
$" from {_Subscribedetails.SubscribeStartDate} to {_Subscribedetails.SubscribeEndDate}");
}
}
I resolved using below code:
container.RegisterType<IBaseOrderProcessing, BaseSubscribe>("Subscribe", new InjectionConstructor(new OOrderProc.Common.SubscribeService.SubscribeService((SubscribeDetails)obj)));
return container.Resolve<IBaseOrderProcessing>("Subscribe");
Long story short, i have following class:
public class FlowBasePipeline<T>
{
private List<StepBaseBusiness<T>> stepList = new List<StepBaseBusiness<T>>();
public void Push(StepBaseBusiness<T> step)
{
stepList.Add(step);
}
public void Trigger(T result)
{
foreach (var step in stepList )
{
result = step.Execute(result);
if (!result.IsSuccess)
{
break;
}
}
}
}
What I'm looking for is forcing programmer to call Push method in the first place and then give them access to Trigger method, in this case following scenario is not allowed
var pipeline=new FlowBasePipeline<MyStepResult>();
pipeline.Trigger()// Trigger method is not recognized
we should first call Push method
var pipeline=new FlowBasePipeline<MyStepResult>();
pipeline.Push(new MyStep()).Trigger()//Now Trigger is recognized
What I've done:
I applied explicit interface method implementation as follows to get it to work:
public interface IBasePipeline<T> where T:BaseResult,new()
{
void Trigger();
IBasePipeline<T> Push(StepBaseBusiness<T> step);
}
public class FlowBasePipeline<T>:IBasePipeline<T> where T:BaseResult,new()
{
private List<StepBaseBusiness<T>> stepList = new List<StepBaseBusiness<T>>();
public IBasePipeline<T> Push(StepBaseBusiness<T> step)
{
stepList.Add(step);
return this;
}
void IBasePipeline<T>.Trigger(T result)
{
foreach (var step in stepList )
{
result = step.Execute(result);
if (!result.IsSuccess)
{
break;
}
}
}
}
Now it works well and we don't have access to Trigger method before Push method, but from my prospective it's not a good way as we might need more level of orders and i don't know how it could be done in this way.
As i know, method chaining is one of the key rules of functional programming.
Is there any pattern or strategy to implement this kind of chaining?
Update:
we need to call push method multiple times
var pipeline=new FlowBasePipeline<MyStepResult>();
pipeline.Push(new MyStep1()).Push(new MyStep2()).Trigger();
After first push, push and trigger would be available.
One way to do this is to use interfaces to restrict access to specific methods by specifying an interface as the result.
public interface IStartCar
{
IDriveCar Start(string key);
}
public interface IDriveCar
{
IParkCar Drive(string address);
}
public interface IParkCar
{
IStopCar Park();
}
public interface IStopCar
{
IParkCar Drive(string address);
void Stop();
}
public class Car : IStartCar, IDriveCar, IParkCar, IStopCar
{
public IDriveCar Start(string key);
public IParkCar Drive(string address);
public IStopCar Park();
public IStopCar Park();
private Car() { }
public static IStartCar Get()
{
var result = new Car();
return result;
}
}
Now to get a Car you use the CarFactory method Get(), it returns a car, but you really only have access to the interface result. This pattern only allows developers to string specific methods together:
var car = Car.Get();
car.Start("key").Drive("address1").Park().Drive("address2").Park().Stop();
Example of approach I use for api's that inherently 'guide' the callers with a fluent syntax:
public class Pipeline
{
readonly List<Action> _steps = new List<Action>();
// only Push is available when Pipeline is initialized
public PipelineWithSteps Push(Action step)
{
_steps.Add(step);
// or cache this if you want 'Push' repeatable
return new PipelineWithSteps(this);
}
public class PipelineWithSteps
{
// not required but often the chained context wants/needs access to the first context
readonly Pipeline _context;
// api is public but ctor cannot be invoked by external caller
internal PipelineWithSteps(Pipeline context) => _context = context;
// now Trigger is available only after something was pushed
public PipelineWithSteps Trigger()
{
foreach(var step in _context._steps)
step();
Console.WriteLine();
return this;
}
// usually I don't repeat an initialization method;
// this could be done using the 'context'
// but would have to be refactored to return the existing 'PipelineWithSteps'
public PipelineWithSteps Push(Action step)
{
_context._steps.Add(step);
return this;
}
}
}
Usage:
var pipeline = new Pipeline();
pipeline.Push(() => Console.WriteLine("A"))
.Push(() => Console.WriteLine("B"))
.Trigger()
.Push(() => Console.WriteLine("C"))
.Trigger();
Output:
A
B
A
B
C
After hours ,i came up with this design:
public interface IBasePipelineRegister<T> where T:BaseResult
{
IStagePipeline<T> Push(StepBaseBusiness<T> step);
List<StepBaseBusiness<T>> Steps { get; set; }
}
public interface IBasePipelineTrigger<T> where T:BaseResult
{
void Trigger(T result);
}
public interface IStagePipeline<T>: IBasePipelineTrigger<T>,IBasePipelineRegister<T> where T:BaseResult
{
}
public class FlowBasePipeline<TResult> : IBasePipelineRegister<TResult> where TResult : BaseResult,new()
{
public List<StepBaseBusiness<TResult>> Steps { get ; set ; }
private IStagePipeline<TResult> _stagePipeline;
public BasePipeline()
{
this.Steps = new List<StepBaseBusiness<TResult>>();
this._stagePipeline = new StagePipeline<TResult>(this);
}
public IStagePipeline<TResult> Push(StepBaseBusiness<TResult> step)
{
Steps.Add(step);
return _stagePipeline;
}
}
As you see, BasePipeline just implements IBasePipelineRegister and Register method presents new StagePipeline class that is consist of current class plus trigger implementation.
public class StagePipeline<T>: IStagePipeline<T> where T:BaseResult
{
private readonly IBasePipelineRegister<T> pipelineRegister;
public List<StepBaseBusiness<T>> Steps { get; set; }
public StagePipeline(IBasePipelineRegister<T> pipelineRegister)
{
this.pipelineRegister = pipelineRegister;
Steps = pipelineRegister.Steps;
}
public IStagePipeline<T> Push(StepBaseBusiness<T> step)
{
return pipelineRegister.Push(step);
}
public void Trigger(T result)
{
foreach (var step in Steps)
{
result = step.Execute(result);
if (!result.IsSuccess)
{
break;
}
}
}
}
Now each method adds a new feature not replacing new one.
var pipeline=new FlowBasePipeline<MyStepResult>();
pipeline.Push(new MyStep1()).Push(new MyStep2()).Trigger();
I need to migrate some data in an Orchard module running within a Orchard 1.9.0 installation. The issue here is that the data is stored in a foreign DB on another server, not in the Orchard DB. So when the migration class methods get called, internally Orchard uses the Orchard.Data.ISessionLocator interface to retrieve the DB connection. Sadly overriding this behavior is not possible but i had the idea to hook into the session locator thing by creating a custom session locator.
The custom session locator looks like this and is based on the existing class Orchard.Data.SessionLocator:
public class CustomSessionLocator : Orchard.Data.ISessionLocator, Orchard.Data.ITransactionManager, System.IDisposable
{
// public
public CustomSessionLocator(Orchard.Data.ISessionFactoryHolder aSessionFactoryHolder)
{
Logger = Orchard.Logging.NullLogger.Instance;
IsolationLevel = System.Data.IsolationLevel.ReadCommitted;
mSessionFactoryHolder = aSessionFactoryHolder;
mSessions = new System.Collections.Generic.Dictionary<SessionScope, NHibernate.ISession>();
if (mForeignDBConnection == null)
{
string lConnectionString = "data source=myServer;initial catalog=myDB;persist security info=True;user id=xxx;password=xxx;MultipleActiveResultSets=True;";
mForeignDBConnection = new System.Data.SqlClient.SqlConnection(lConnectionString);
}
}
public NHibernate.ISession For(System.Type aEntityType)
{
Logger.Debug("Acquiring session for {0}", aEntityType);
Demand();
return mSessions[CurrentSessionScope];
}
public void Demand()
{
EnsureSession(IsolationLevel);
}
public void RequireNew()
{
RequireNew(IsolationLevel);
}
public void RequireNew(System.Data.IsolationLevel aLevel)
{
DisposeSession();
EnsureSession(aLevel);
}
public void Cancel()
{
NHibernate.ISession lSession;
if (mSessions.TryGetValue(CurrentSessionScope, out lSession) && lSession != null && !lSession.Transaction.WasRolledBack && lSession.Transaction.IsActive)
{
Logger.Debug("Rolling back transaction");
lSession.Transaction.Rollback();
}
}
public void Dispose()
{
DisposeSession();
}
public enum SessionScope
{
OrchardDefault,
ForeignDB
}
public Orchard.Logging.ILogger Logger { get; set; }
public System.Data.IsolationLevel IsolationLevel { get; set; }
public SessionScope CurrentSessionScope { private get; set; }
// private
private void DisposeSession()
{
NHibernate.ISession lSession;
if (mSessions.TryGetValue(CurrentSessionScope, out lSession) && lSession != null)
{
try
{
if (!lSession.Transaction.WasRolledBack && lSession.Transaction.IsActive)
{
Logger.Debug("Committing transaction");
lSession.Transaction.Commit();
}
}
finally
{
Logger.Debug("Disposing session");
var lConnection = lSession.Connection;
lSession.Close();
lSession.Dispose();
lSession = null;
mSessions[CurrentSessionScope] = null;
}
}
}
private void EnsureSession(System.Data.IsolationLevel aLevel)
{
NHibernate.ISession lSession;
if (mSessions.TryGetValue(CurrentSessionScope, out lSession) && lSession != null)
return;
var lSessionFactory = mSessionFactoryHolder.GetSessionFactory();
Logger.Debug("Opening NHibernate session");
if (CurrentSessionScope == SessionScope.ForeignDB)
{
lSession = lSessionFactory.OpenSession(mForeignDBConnection);
// open connection otherwise the following lSession.BeginTransaction() fails with exception
if (mForeignDBConnection.State == System.Data.ConnectionState.Closed)
mForeignDBConnection.Open();
}
else
lSession = lSessionFactory.OpenSession();
mSessions[CurrentSessionScope] = lSession;
lSession.BeginTransaction(aLevel);
}
private readonly Orchard.Data.ISessionFactoryHolder mSessionFactoryHolder;
private System.Collections.Generic.Dictionary<SessionScope, NHibernate.ISession> mSessions;
private static System.Data.SqlClient.SqlConnection mForeignDBConnection;
}
Then i have a migration data interpreter that looks like this:
public class ForeignDataMigrationInterpreter : Orchard.Data.Migration.Interpreters.DefaultDataMigrationInterpreter
{
public ForeignDataMigrationInterpreter(
Orchard.Environment.Configuration.ShellSettings aShellSettings,
Orchard.Data.ISessionLocator aSessionLocator,
System.Collections.Generic.IEnumerable<Orchard.Data.Migration.Interpreters.ICommandInterpreter> aCommandInterpreters,
Orchard.Data.ISessionFactoryHolder aSessionFactoryHolder,
Orchard.Reports.Services.IReportsCoordinator aReportsCoordinator)
: base(aShellSettings, aSessionLocator, aCommandInterpreters, aSessionFactoryHolder, aReportsCoordinator)
{
mSessionLocator = aSessionLocator as CustomSessionLocator;
}
public override void Visit(Orchard.Data.Migration.Schema.CreateTableCommand aCommand)
{
#if LIVE
if (IsForeignDBCommand(aCommand.Name, ""))
mSessionLocator.CurrentSessionScope = CustomSessionLocator.SessionScope.ForeignDB;
else
mSessionLocator.CurrentSessionScope = CustomSessionLocator.SessionScope.OrchardDefault;
#endif
base.Visit(aCommand);
}
...
private bool IsForeignDBCommand(...)
{
return ...;
}
private CustomSessionLocator mSessionLocator;
}
As you can see, the basic procedure with foreign data is
Start Orchard
Migration class method is called which contains SchemaBuilder.CreateTable()
ForeignDataMigrationInterpreter.Visit(CreateTableCommand) is called
CurrentSessionScope of custom session locator is updated to SessionScope.ForeignDB
CreateTableCommand is passed into base class
CustomSessionLocator.For() is called which
ends in CustomSessionLocator.EnsureSession() which
returns the session X for scope SessionScope.ForeignDB
base class enlists CreateTableCommand to transaction of session X
fast forward some unrelated additional steps and the transaction is commited BUT it never returns and a timeout exception occures
My questions are
Is it even possible to migrate foreign data this way?
Why does timeout occur?
I have an application which caches some data at startup. There are several things to put in a cache, but they are very similar. I created classes like this, the only difference in them is the type of the item to be added to the dictionary (in this example the Setting class), and the _sqlNotifyCommand.CommandText.
public class SettingsCache : ILoggerClass
{
private Dictionary<int, Dictionary<int, Setting>> _cachedItems;
private string _entityConnectionString;
private SQLNotifier _sqlNotifier;
private SqlCommand _sqlNotifyCommand = new SqlCommand();
private bool _dataLoaded = false;
private void AddItem(Setting item)
{
if (!_cachedItems.ContainsKey(item.PartnerId))
{
_cachedItems.Add(item.PartnerId, new Dictionary<int, Setting>());
}
if (_cachedItems[item.PartnerId].ContainsKey(item.Id))
{
_cachedItems[item.PartnerId].Remove(item.Id);
}
_cachedItems[item.PartnerId].Add(item.Id, item);
}
public Setting GetSetting(int partnerId, int id)
{
if (_cachedItems.ContainsKey(partnerId))
{
if (_cachedItems[partnerId].ContainsKey(id))
{
return _cachedItems[partnerId][id];
}
return null;
}
return null;
}
public SettingsCache(string connectionString)
{
_entityConnectionString = connectionString;
_cachedItems = new Dictionary<int, Dictionary<int, Setting>>();
LoadData();
try
{
using (var db = new partnerEntity(connectionString))
{
string adoSqlConnectionString = ((EntityConnection) db.Connection).StoreConnection.ConnectionString;
_sqlNotifier = new SQLNotifier(adoSqlConnectionString);
_sqlNotifier.NewMessage += _sqlNotifier_NewMessage;
_sqlNotifyCommand.CommandType = CommandType.Text;
_sqlNotifyCommand.CommandText = "SELECT setting_id, setting_value, partner_id FROM dbo.setting";
_sqlNotifyCommand.Notification = null;
_sqlNotifier.RegisterDependency(_sqlNotifyCommand);
}
}
catch (Exception exception)
{
this.Log(this, LogLevel.Error, 0, exception);
}
}
private void _sqlNotifier_NewMessage(object sender, SqlNotificationEventArgs e)
{
if (e.Info == SqlNotificationInfo.Insert || e.Info == SqlNotificationInfo.Update)
{
this.Log(this, LogLevel.Info, 0, string.Format("Database changed, reloading settings data..."));
LoadData();
}
_sqlNotifier.RegisterDependency(_sqlNotifyCommand);
}
private void LoadData()
{
_dataLoaded = false;
try
{
using (var db = new partnerEntity(_entityConnectionString))
{
var dbData = db.setting.ToList();
foreach (var cItem in dbData)
{
AddItem(new Setting
{
PartnerId = cItem.partner_id,
Id = cItem.setting_id,
Value = cItem.setting_value
});
}
}
_dataLoaded = true;
}
catch (Exception exception)
{
this.Log(this, LogLevel.Error, 0, exception);
}
if (!_dataLoaded)
{
Task.Delay(TimeSpan.FromSeconds(10)).ContinueWith(_ => { LoadData(); });
}
}
}
Is there a more generic way to do this? The last thing which was needed in the classes this part:
if (!_dataLoaded)
{
Task.Delay(TimeSpan.FromSeconds(10)).ContinueWith(_ => { LoadData(); });
}
And I had to modify every Caching class. I needed to declare the variable, add it to the try-catch block, and after the block insert the same line in 6 classes. This code seems very boilerplate to me, I can't believe there is no simpler solution. I tried to make an Interface with AddItem, LoadData, OnLoadDataFailed methods, but in the AddItem method I need to specify the item, I'm stuck.
Here is the basic structure of what (I think) you want to accomplish using Generics.
First, declare a common interface for cached items so that the AddItem method still works:
public interface ICacheItem
{
int Id { get; set; }
int PartnerId { get; set; }
}
Now you can create the SettingsCache<T> that holds the items:
public class SettingsCache<T> : ILogger where T : ICacheItem
{
private Dictionary<int, Dictionary<int, T>> _cachedItems;
//...
public SettingsCache(string connectionString, string commandText)
{
//...
}
private void AddItem(T item)
{
if (!_cachedItems.ContainsKey(item.PartnerId))
{
_cachedItems.Add(item.PartnerId, new Dictionary<int, T>());
}
if (_cachedItems[item.PartnerId].ContainsKey(item.Id))
{
_cachedItems[item.PartnerId].Remove(item.Id);
}
_cachedItems[item.PartnerId].Add(item.Id, item);
}
}
I left out most of the implementation to focus on your two biggest concerns, the command text and the AddItem method. The generic has a constraint so that it can only accept items that are of ICacheItem, so you can use any properties that ICacheItem defines.
To use a cache, simply create one with the specific type (assuming Setting implements ICacheItem):
var settingsCache = new SettingsCache<Setting>("connection string", "command string");
Based on this page we've created a Wizard that has three steps. Everything works great, but we have one problem with the code given in the link, which is how it creates the next step instance (copy pasted from the link):
protected override IScreen DetermineNextItemToActivate(IList<IScreen> list, int lastIndex)
{
var theScreenThatJustClosed = list[lastIndex] as BaseViewModel;
var state = theScreenThatJustClosed.WorkflowState;
var nextScreenType = TransitionMap.GetNextScreenType(theScreenThatJustClosed);
var nextScreen = Activator.CreateInstance(nextScreenType, state);
return nextScreen as IScreen;
}
Currently, it looks like this in our project:
protected override IWizardScreen DetermineNextItemToActivate(IList<IWizardScreen> list, int lastIndex)
{
var theScreenThatJustClosed = list[lastIndex];
if (theScreenThatJustClosed == null) throw new Exception("Expecting a screen here!");
if (theScreenThatJustClosed.NextTransition == WizardTransition.Done)
{
TryClose(); // Close the entire Wizard
}
var state = theScreenThatJustClosed.WizardAggregateState;
var nextScreenType = _map.GetNextScreenType(theScreenThatJustClosed);
if (nextScreenType == null) return null;
// TODO: CreateInstance requires all constructors for each WizardStep, even if they aren't needed. This should be different!
var nextScreen = Activator.CreateInstance(nextScreenType, state, _applicationService, _wfdRegisterInstellingLookUp,
_adresService, _userService, _documentStore, _windowManager, _fileStore, _fileUploadService, _dialogService,
_eventAggregator, _aanstellingViewModelFactory);
return nextScreen as IWizardScreen;
}
As you can see, we have quite a few parameters we need in some steps. In step 1 we only need like two, but because of the Activator.CreateInstance(nextScreenType, state, ...); we still need to pass all of them.
What I'd like instead is to use a delegate Factory. We use them at more places in our project, and let AutoFac take care of the rest of the parameters. For each of the three steps we only need a delegate Factory that uses the state.
Because all three uses the same delegate Factory with just state, I've placed this Factory in their Base class:
public delegate WizardBaseViewModel<TViewModel> Factory(AggregateState state);
How I'd like to change the DetermineNextItemToActivate method:
protected override IWizardScreen DetermineNextItemToActivate(IList<IWizardScreen> list, int lastIndex)
{
var theScreenThatJustClosed = list[lastIndex];
if (theScreenThatJustClosed == null) throw new Exception("Expecting a screen here!");
if (theScreenThatJustClosed.NextTransition == WizardTransition.Done)
{
TryClose(); // Close the entire Wizard
}
return _map.GetNextScreenFactoryInstance(state);
}
But now I'm stuck at making the GetNextScreenFactoryInstance method:
public IWizardScreen GetNextScreenFactoryInstance(IWizardScreen screenThatClosed)
{
var state = screenThatClosed.WizardAggregateState;
// This is where I'm stuck. How do I get the instance using the Factory, when I only know the previous ViewModel
// ** Half-Pseudocode
var nextType = GetNextScreenType(screenThatClosed);
var viewModelFactory = get delegate factory based on type?;
var invokedInstance = viewModelFactory.Invoke(state);
// **
return invokedInstance as IWizardScreen;
}
Feel free to change the GetNextScreenFactoryInstance any way you'd like. As long as I can get the next Step-ViewModel based on the previous one in the map.
NOTE: Other relevant code, can be found in the link, but I'll post it here as well to keep it all together:
The WizardTransitionMap (only change is it not being a Singleton anymore, so we can instantiate a map outselves):
public class WizardTransitionMap : Dictionary<Type, Dictionary<WizardTransition, Type>>
{
public void Add<TIdentity, TResponse>(WizardTransition transition)
where TIdentity : IScreen
where TResponse : IScreen
{
if (!ContainsKey(typeof(TIdentity)))
{
Add(typeof(TIdentity), new Dictionary<WizardTransition, Type> { { transition, typeof(TResponse) } });
}
else
{
this[typeof(TIdentity)].Add(transition, typeof(TResponse));
}
}
public Type GetNextScreenType(IWizardScreen screenThatClosed)
{
var identity = screenThatClosed.GetType();
var transition = screenThatClosed.NextTransition;
if (!transition.HasValue) return null;
if (!ContainsKey(identity))
{
throw new InvalidOperationException(String.Format("There are no states transitions defined for state {0}", identity));
}
if (!this[identity].ContainsKey(transition.Value))
{
throw new InvalidOperationException(String.Format("There is no response setup for transition {0} from screen {1}", transition, identity));
}
return this[identity][transition.Value];
}
}
Our InitializeMap-method:
protected override void InitializeMap()
{
_map = new WizardTransitionMap();
_map.Add<ScreenOneViewModel, ScreenTwoViewModel>(WizardTransition.Next);
_map.Add<ScreenTwoViewModel, ScreenOneViewModel>(WizardTransition.Previous);
_map.Add<ScreenTwoViewModel, ScreenThreeViewModel>(WizardTransition.Next);
_map.Add<ScreenThreeViewModel, ScreenTwoViewModel>(WizardTransition.Previous);
_map.Add<ScreenThreeViewModel, ScreenThreeViewModel>(WizardTransition.Done);
}
We've changed the code:
The WizardTransitionMap now accepts Delegates. Also, instead of retrieving the type by the WizardTransition-enum value (Next, Previous, etc.), we now retrieve the Factory-invoke based on the next Type (so the inner Dictionary is reversed). So, this is our current WizardTransitionMap:
using System;
using System.Collections.Generic;
namespace NatWa.MidOffice.CustomControls.Wizard
{
public class WizardTransitionMap : Dictionary<Type, Dictionary<Type, Delegate>>
{
public void Add<TCurrentScreenType, TNextScreenType>(Delegate delegateFactory)
{
if (!ContainsKey(typeof(TCurrentScreenType)))
{
Add(typeof(TCurrentScreenType), new Dictionary<Type, Delegate> { { typeof(TNextScreenType), delegateFactory } });
}
else
{
this[typeof(TCurrentScreenType)].Add(typeof(TNextScreenType), delegateFactory);
}
}
public IWizardScreen GetNextScreen(IWizardScreen screenThatClosed)
{
var identity = screenThatClosed.GetType();
var state = screenThatClosed.State;
var transition = screenThatClosed.NextScreenType;
if (!ContainsKey(identity))
{
throw new InvalidOperationException(String.Format("There are no states transitions defined for state {0}", identity));
}
if (!this[identity].ContainsKey(transition))
{
throw new InvalidOperationException(String.Format("There is no response setup for transition {0} from screen {1}", transition, identity));
}
if (this[identity][transition] == null)
return null;
return (IWizardScreen)this[identity][transition].DynamicInvoke(state);
}
}
}
Our InitializeMap is now changed to this:
protected override void InitializeMap()
{
_map = new WizardTransitionMap();
_map.Add<ScreenOneViewModel, ScreenTwoViewModel>(_screenTwoFactory);
_map.Add<ScreenTwoViewModel, ScreenOneViewModel>(_screenOneFactory);
_map.Add<ScreenTwoViewModel, ScreenThreeViewModel>(_screenThreeFactory);
_map.Add<ScreenThreeViewModel, ScreenTwoViewModel>(_screenTwoFactory);
_map.Add<ScreenThreeViewModel, ScreenThreeViewModel>(null);
}
And our DetemineNexttemToActivate method to this:
protected override IWizardScreen DetermineNextItemToActivate(IList<IWizardScreen> list, int previousIndex)
{
var theScreenThatJustClosed = list[previousIndex];
if (theScreenThatJustClosed == null) throw new Exception("Expecting a screen here!");
var nextScreen = _map.GetNextScreen(theScreenThatJustClosed);
if (nextScreen == null)
{
TryClose();
return ActiveItem; // Can't return null here, because Caliburn's Conductor will automatically get into this method again with a retry
}
return nextScreen;
}
We also removed our entire WizardBaseViewModel and just let every Step-ViewModel implement the IWizardScreen:
public interface IWizardScreen : IScreen
{
AggregateState State { get; }
Type NextScreenType { get; }
void Next();
void Previous();
}
With the following implementation in our ScreenOneViewModel:
public AggregateState State { get { return _state; } }
public Type NextScreenType { get; private set; }
public void Next()
{
if (!IsValid()) return;
NextScreenType = typeof(ScreenTwoViewModel);
TryClose();
}
public void Previous()
{
throw new NotImplementedException(); // Isn't needed in first screen, because we have no previous
}
And the following implementation in our ScreenThreeViewModel:
public AggregateState State { get { return _state; } }
public Type NextScreenType { get; private set; }
public void Next()
{
NextScreenType = typeof(ScreenThreeViewModel); // Own type, because we have no next
TryClose();
}
public void Previous()
{
NextScreenType = typeof(ScreenTwoViewModel);
TryClose();
}
And each Step-ViewModel has its own delegate Factory, like this one for ScreenTwoViewModel:
public delegate ScreenTwoViewModel Factory(AggregateState state);