我有一个完整的引擎,它依赖于基于用户交互的抽象。这适用于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注入弹出窗口,但我不知道如何在不破坏所有进程的情况下执行此操作...
有人做过这个吗?
答案 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");
}
}
}
从ready
或load
事件:
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;
}
}
说明:
AddSocket
方法SendAsync
方法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调用控制器操作以应用逻辑。
澄清......
所以,假设您正在构建基于用户回答的一系列问题的流程,并将这些问题放入流程中。
你可以......
然后做一个执行构建过程的帖子。
想到&#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
}
});
});
抱歉没有完全理解这个问题。
希望这有帮助!