如何在ASP MVC中实现“用户交互”依赖注入

时间:2017-02-28 20:03:04

标签: c# asp.net asp.net-mvc dependency-injection

我有一个完整的引擎,它依赖于基于用户交互的抽象。这适用于WPF / Xamarin应用程序,因为我可以用窗口/窗体实现这种抽象。

将此引擎移植到ASP MVC中我有一点问题。

可以这样显示一个简单的例子。

抽象界面(简化)

public interface IQuestionBox
{
    Task<bool> ShowYesNoQuestionBox(string message);
}

对于WPF,它非常简单,我实现这个接口,通过调用ShowDialog()返回窗口的结果。

在一个简单的商务课程中,我可以进行这种调用(简化):

public async Task<string> GetValue(IQuestionBox qbox)
{
    if(await qbox.ShowYesNoQuestionBox("Question ?"))
    {
        return "Ok";
    }
    return "NOk";
}

我真的不知道如何在ASP中实现这种行为,因为无状态的HTTP,知道这种呼叫可以像域/业务需要那样多样化。我认为应该这样做的方法是返回一个PartialView注入弹出窗口,但我不知道如何在不破坏所有进程的情况下执行此操作...

有人做过这个吗?

4 个答案:

答案 0 :(得分:2)

正如我所说的那样,我强烈建议不要这样做,但是可能的话,请允许这样做的代码,让我们走吧:

为了成为可能我滥用TaskCompletionSource的用法,这个类允许我们手动设置任务结果。

  • 首先,我们需要创建一个封装过程的结构:

    public class Process
    {
         // this dictionary store the current process running status, you will use it to define the future answer from the user interaction
         private static Dictionary<string, Answare> StatusReport = new Dictionary<string, Answare>();
         // this property is the secret to allow us wait for the ShowYesNoQuestion call, because til this happen the server doesn't send a response for the client.
         TaskCompletionSource<bool> AwaitableResult { get; } = new      TaskCompletionSource<bool>(true);
         // here we have the question to interact with the user
         IQuestionBox QuestionBox { get; set; }
    
         // this method, receive your bussiness logical the receive your question as a parameter
         public IQuestionBox Run(Action<IQuestionBox> action)
         {
             QuestionBox = new QuestionBox(this);
             // here we create a task to execute your bussiness logical processment
             Task.Factory.StartNew(() =>
             {
                action(QuestionBox);
             });
             // and as I said we wait the result from the processment
             Task.WaitAll(AwaitableResult.Task);
             // and return the question box to show the messages for the users
             return QuestionBox;
         }
    
         // this method is responsable to register a question to receive future answers, as you can see, we are using our static dictionary to register them
         public void RegisterForAnsware(string id)
         {
            if (StatusReport.ContainsKey(id))
               return;
            StatusReport.Add(id, new Answare()
            {
            });
         }
    
         // this method will deliver an answer for this correct context based on the id
         public Answare GetAnsware(string id)
         {
             if (!StatusReport.ContainsKey(id))
               return Answare.Empty;
             return StatusReport[id];
         }
    
         // this method Releases the processment
         public void Release()
         {
             AwaitableResult.SetResult(true);
         }
    
         // this method end the process delivering the response for the user
         public void End(object userResponse)
         {
            if (!StatusReport.ContainsKey(QuestionBox.Id))
               return;
            StatusReport[QuestionBox.Id].UserResponse(userResponse);
         }
    
         // this method define the answer based on the user interaction, that allows the process continuing from where it left off
         public static Task<object> DefineAnsware(string id, bool result)
         {
             if (!StatusReport.ContainsKey(id))
               return Task.FromResult((object)"Success on the operation");
             // here I create a taskcompletaionsource to allow get the result of the process, and send for the user, without it would be impossible to do it
             TaskCompletionSource<object> completedTask = new           TaskCompletionSource<object>();
             StatusReport[id] = new Answare(completedTask)
             {
                HasAnswared = true,
                Value = result
             };
             return completedTask.Task;
         }
    }
    
  • 之后的问题实现

    public interface IQuestionBox
    {
        string Id { get; }
        Task<bool> ShowYesNoQuestionBox(string question);
        HtmlString ShowMessage();
    }
    
    class QuestionBox : IQuestionBox
    {
        Process CurrentProcess { get; set; }
    
        public string Id { get; } = Guid.NewGuid().ToString();
        private string Question { get; set; }
    
        public QuestionBox(Process currentProcess)
        {
            CurrentProcess = currentProcess;
            CurrentProcess.RegisterForAnswer(this.Id);
        }
    
        public Task<bool> ShowYesNoQuestionBox(string question)
        {
            Question = question;
            CurrentProcess.Release();
            return AwaitForAnswer();
        }
    
        public HtmlString ShowMessage()
        {
            HtmlString htm = new HtmlString(
                $"<script>showMessage('{Question}', '{Id}');</script>"
            );
    
            return htm;
        }
    
        private Task<bool> AwaitForAnswer()
        {
            TaskCompletionSource<bool> awaitableResult = new TaskCompletionSource<bool>(true);
    
            Task.Factory.StartNew(() =>
            {
                while (true)
                {
                    Thread.Sleep(2000);
                    var answare = CurrentProcess.GetAnswer(this.Id);
                    if (!answare.HasAnswered)
                        continue;
                    awaitableResult.SetResult(answare.Value);
                    break;
                }
            });
    
            return awaitableResult.Task;
        }
    }
    

您实施的差异是:

1 - 我创建了一个标识符,以了解我必须向谁发送aswer,或者只是为了停止该过程。

2 - 我收到一个Process as参数,因为这允许我们调用该方法 CurrentProcess.Release();在ShowYesNoQuestion中,具体来说,释放发送响应的过程,负责与用户交互。

3 - 我创建方法AwaitForAnswer,这是我们在TaskCompletionSource类中再次使用的方法。正如你在这个方法中看到的那样,我们有一个循环,这个循环负责等待用户交互,直到收到它不释放进程的响应。

4 - 我创建ShowMessage方法,创建一个简单的html脚本警报来模拟用户交互。

  • 然后是一个简单的流程类,因为你应该在你的业务逻辑中:

    public class SaleService
    {
       public async Task<string> GetValue(IQuestionBox qbox)
       {
          if (await qbox.ShowYesNoQuestionBox("Do you think Edney is the big guy ?"))
          {
               return "I knew, Edney is the big guy";
          }
          return "No I disagree";
       }
    }
    
  • 然后该类代表用户回答

    public class Answer
    {
        // just a sugar to represent empty responses
        public static Answer Empty { get; } = new Answer { Value = true, HasAnswered = true };
    
        public Answer()
        {
    
        }
    
        // one more time abusing from TaskCompletionSource<object>, because with this guy we are abble to send the result from the process to the user
        public Answer(TaskCompletionSource<object> completedTask)
        {
            CompletedTask = completedTask;
        }
    
        private TaskCompletionSource<object> CompletedTask { get; set; }
    
        public bool Value { get; set; }
        public bool HasAnswered { get; set; }
    
        // this method as you can see, will set the result and release the task for the user
        public void UserResponse(object response)
        {
            CompletedTask.SetResult(response);
        }
    }
    
  • 现在我们使用所有整个结构创建:

    [HttpPost]
    public IActionResult Index(string parametro)
    {
        // create your process an run it, passing what you want to do
        Process process = new Process();
        var question = process.Run(async (questionBox) =>
        {
            // we start the service
            SaleService service = new SaleService();
            // wait for the result
            var result = await service.GetValue(questionBox);
            // and close the process with the result from the process
            process.End(result);
        });
    
        return View(question);
    }
    
    // here we have the method that deliver us the user response interaction
    [HttpPost]
    public async Task<JsonResult> Answer(bool result, string id)
    {
        // we define the result for an Id on the process
        var response = await Process.DefineAnswer(id, result);
        // get the response from process.End used bellow
        // and return to the user
        return Json(response);
    }
    
  • 并在您的视图中

     <!-- Use the question as the model  page -->
     @model InjetandoInteracaoComUsuario.Controllers.IQuestionBox
     <form asp-controller="Home" asp-action="Index">
         <!-- create a simple form with a simple button to submit the home -->
         <input type="submit" name="btnDoSomething" value="All about Edney" />
     </form>
     <!-- in the scripts section we create the function that we call on the method ShowMessage, remember?-->
     <!-- this method request the action answer passing the questionbox id, and the result from a simple confirm -->
     <!-- And to finalize, it just show an  alert with the process result -->
     @section scripts{
     <script>
          function showMessage(message, id) {
             var confirm = window.confirm(message);
             $.post("/Home/Answer", { result: confirm, id: id }, function (e) {
                alert(e);
             })
          }
    
     </script>
     @Model?.ShowMessage()
    }
    

正如我所说,我真的不同意这些实践,正确的应该写一个新的dll,以支持网络环境,但我希望它能帮到你。

我把项目放在github上你可以 download 了解所有解决方案

我真的希望它可以帮到你

答案 1 :(得分:1)

您可以创建从客户端到服务器端的Web套接字连接。并使用Web套接字请求处理前端内容。它可以实现如下:

客户方:

$app = {
    uiEventsSocket : null,
    initUIEventsConnection : function(url) {
        //create a web socket connection
        if (typeof (WebSocket) !== 'undefined') {
                this.uiEventsSocket = new WebSocket(url);
            } else if (typeof (MozWebSocket) !== 'undefined') {
                this.uiEventsSocket = new MozWebSocket(url);
            } else {
                console.error('WebSockets unavailable.');
            }

            //notify if there is an web socket error
            this.uiEventsSocket.onerror = function () {
                console.error('WebSocket raised error.');
            }

            this.uiEventsSocket.onopen = function () {
                console.log("Connection to " + url + " established");
            }

            //handling message from server side
            this.uiEventsSocket.onmessage = function (msg) {
                this._handleMessage(msg.data);
            };
    },

    _handleMessage : function(data){
        //the message should be in json format
        //the next line fails if it is not
        var command = JSON.parse(data);

        //here is handling the request to show prompt
        if (command.CommandType == 'yesNo') {
            var message = command.Message;

            var result = confirm(message);
            //not sure that bool value will be successfully converted
            this.uiEventsSocket.send(result ? "true" : "false");
        }
    }
}

readyload事件:

启动它
window.onload = function() { $app.initUIEventsConnection(yourUrl); }

请注意,您的网址应以ws://而不是http://wss://开头,而不是https://(网页套接字和网络套接字安全)。

服务器端。

Here是一篇关于如何在asp.net核心应用程序中设置Web套接字的好文章,或者您可以找到另一个。请注意,您应该对来自单个用户的Web套接字连接进行分组,如果要向具体用户发送消息,则应该为该用户的每个连接发送消息。

您应该使用AcceptWebSocketAsync()方法调用接受每个Web套接字,然后将此Web套接字的实例添加到singleton,其中包含一组由用户分组的Web套接字连接。

以下类将用于操作命令:

public class UICommand
{
    public string CommandType { get; set; }
    public string Message { get; set; }
    public Type ReturnType { get; set; }
}

用于处理套接字的完整单例代码

public class WebSocketsSingleton
{
    private static WebSocketsSingleton _instance = null;
    //here stored web sockets groupped by user
    //you could use user Id or another marker to exactly determine the user
    private Dictionary<string, List<WebSocket>> _connectedSockets;

    //for a thread-safety usage
    private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim();

    public static WebSocketsSingleton Instance {
        get {
            if (this._instance == null)
            {
                this._instance = new WebSocketsSingleton();
            }

            return this._instance;
        }
    }

    private WebSocketsSingleton()
    {
        this._connectedSockets = new Dictionary<string, List<WebSocket>>();
    }

    /// <summary>
    /// Adds a socket into the required collection
    /// </summary>
    public void AddSocket(string userName, WebSocket ws)
    {
        if (!this._connectedSockets.ContainsKey(userName))
        {
            Locker.EnterWriteLock();
            try
            {
                this._connectedSockets.Add(userName, new List<WebSocket>());
            }
            finally
            {
                Locker.ExitWriteLock();
            }
        }

        Locker.EnterWriteLock();
        try
        {
            this._connectedSockets[userName].Add(ws);
        }
        finally
        {
            Locker.ExitWriteLock();
        }
    }

    /// <summary>
    /// Sends a UI command to required user
    /// </summary>  
    public async Task<string> SendAsync(string userName, UICommand command)
    {
        if (this._connectedSockets.ContainsKey(userName))
        {
            var sendData = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(command));

        foreach(var item in this._connectedSockets[userName])
        {
            try
            {
                await item.SendAsync(new ArraySegment<byte>(sendData), WebSocketMessageType.Text, true, CancellationToken.None);
            }
            catch (ObjectDisposedException)
            {
                //socket removed from front end side
            }
        }

            var buffer = new ArraySegment<byte>(new byte[1024]);
            var token = CancellationToken.None;         
            foreach(var item in this._connectedSockets[userName])
            {
                await Task.Run(async () => {
                    var tempResult = await item.ReceiveAsync(buffer, token);
                    //result received
                    token = new CancellationToken(true);
                });
            }

            var resultStr = Encoding.Utf8.GetString(buffer.Array);

            if (command.ReturnType == typeof(bool))
            {
                return resultStr.ToLower() == "true";
            }

            //other methods to convert result into required type

            return resultStr;
        }

        return null;
    }
}

说明:

  • 关于从Web套接字建立连接,它将被添加 AddSocket方法
  • 在发送显示消息的请求时,所需命令将传递到SendAsync方法
  • 该命令将序列化为JSON(使用Json.Net,但您可以按照自己的方式序列化)并发送到与所需用户相关的所有套接字
  • 发送命令后,应用程序将等待来自前端的响应
  • 结果将转换为所需类型并发送回您的IQuestionBox

在Web套接字处理中,您应该添加以下代码:

app.Use(async (http, next) =>
{
    if (http.WebSockets.IsWebSocketRequest)
    {
        var webSocket = await http.WebSockets.AcceptWebSocketAsync();
        var userName = HttpContext.Current.User.Identity.Name;
        WebSocketsSingleton.Instance.AddSocket(userName, webSocket);

        while(webSocket.State == WebSocketState.Open)
        {
            //waiting till it is not closed         
        }

        //removing this web socket from the collection
    }
});

ShowYesNoQuestionBox的方法实现应该如下:

public async Task<bool> ShowYesNoQuestionBox(string userName, string text)
{
    var command = new UICommand
    {
        CommandType = "yesNo",
        Message = text,
        ReturnType = typeof(bool)
    };

    return await WebSocketsSingleton.Instance.SendAsync(string userName, command);
}

请注意,应添加userName以防止向所有已连接的用户发送相同的消息。

WebSocket应该在服务器端和客户端之间建立持久连接,这样您就可以通过两种方式发送命令。

我是Asp.Net Core的新手,所以最终的实现可能与此有点不同。

答案 2 :(得分:0)

它实际上大致相同,除了您的UI在很大程度上与HTTP协议断开连接并代理。

你基本上需要构建与WPF代码相同的代码,然后在浏览器中构建ajax调用控制器操作以应用逻辑。

澄清......

所以,假设您正在构建基于用户回答的一系列问题的流程,并将这些问题放入流程中。

你可以......

  1. 在数据库中构建流程
  2. 在服务器上的会话中构建它
  3. 在客户端上将其构建为js对象
  4. 然后做一个执行构建过程的帖子。

    想到&#34;无国籍状态&#34;作为一系列短交互,但您在它们之间保持的状态可以在客户端,数据库中或在Web服务器上登录的用户中完成。

答案 3 :(得分:0)

在您的控制器中,您可以添加一个ActionResult,它将为您的jquery模式弹出请求提供html响应。这是一个例子

public class MController : Controller {
            public ActionResult doWork(requirement IQuestionBox)
            { 
                // model is already modelBound/IOC resolved
                return PartialView("_doWork", requirement );
            }    
        }

//脚本

$(function(){    
        $.ajax({
           url:"/m/doWork",
           type:"get",
           success:function(data){
               $modal.html(data); // bind to modal
           }
        });
});

抱歉没有完全理解这个问题。

希望这有帮助!