Che*_*hev 8 .net c# methods asp.net-mvc object
Okay, this question is going to be a bit long-winded but hopefully it will be an interesting one to you. I do have a specific question at the end but I want to provide plenty of background and context so that you are on the same page as much as possible and can understand my objective. If lengthy questions aren't your style that's totally fine. However, if you would like to contribute all I ask is that you try your best to understand the vision and be on the same page as much as possible.
If you just want to get to the bounty details section, please scroll toward the bottom and find the header "Bounty Update".
我正在使用ASP.NET MVC 3构建一个控制台式的应用程序.如果你需要去那里了解它是如何运作的,它位于http://www.u413.com.概念本身很简单:从客户端接收命令字符串,检查提供的命令是否存在以及命令提供的参数是否有效,执行命令,返回一组结果.
有了这个应用程序,我决定有点创意.终端式应用程序最明显的解决方案是构建世界上最大的IF语句.通过IF语句运行每个命令,并从内部调用相应的函数.我不喜欢这个主意.在应用程序的旧版本中,这是它的操作方式,这是一个巨大的混乱.向应用程序添加功能非常困难.
经过深思熟虑后,我决定构建一个名为命令模块的自定义对象.我们的想法是为每个请求构建此命令模块.模块对象将包含所有可用命令作为方法,然后站点将使用反射来检查用户提供的命令是否与方法名称匹配.命令模块对象位于ICommandModule如下所示的接口后面.
namespace U413.Business.Interfaces
{
/// <summary>
/// All command modules must ultimately inherit from ICommandModule.
/// </summary>
public interface ICommandModule
{
/// <summary>
/// The method that will locate and execute a given command and pass in all relevant arguments.
/// </summary>
/// <param name="command">The command to locate and execute.</param>
/// <param name="args">A list of relevant arguments.</param>
/// <param name="commandContext">The current command context.</param>
/// <param name="controller">The current controller.</param>
/// <returns>A result object to be passed back tot he client.</returns>
object InvokeCommand(string command, List<string> args, CommandContext commandContext, Controller controller);
}
}
Run Code Online (Sandbox Code Playgroud)
该InvokeCommand()方法是我的MVC控制器立即知道的命令模块上唯一的方法.然后,此方法负责使用反射并查看其自身的实例并找到所有可用的命令方法.
我使用Ninject进行依赖注入.我的MVC控制器具有构造函数依赖性ICommandModule.我构建了一个自定义Ninject提供程序,在解析ICommandModule依赖项时构建此命令模块.Ninject可以构建4种类型的命令模块:
VisitorCommandModuleUserCommandModuleModeratorCommandModuleAdministratorCommandModule还有一个类BaseCommandModule,所有其他模块类都继承自该类.真的很快,这里是继承关系:
BaseCommandModule : ICommandModuleVisitorCommandModule : BaseCommandModuleUserCommandModule : BaseCommandModuleModeratorCommandModule : UserCommandModuleAdministratorCommandModule : ModeratorCommandModule希望你现在可以看到它是如何构建的.根据用户的成员资格状态(未登录,常规用户,主持人等),Ninject将为正确的命令模块提供用户应有权访问的命令方法.
所有这一切都很棒.当我解析命令字符串并弄清楚如何在命令模块对象上构造命令方法时,我的困境就出现了.
How should the command string be parsed and executed?
Currently I break up the command string (the string passed in by the user containing the command and all arguments) in the MVC controller. I then call the InvokeCommand() method on my injected ICommandModule and I pass in a string command and a List<string> args.
Let's say I have the following command:
TOPIC <id> [page #] [reply “reply”]
Run Code Online (Sandbox Code Playgroud)
This line defines the TOPIC command accepting a required ID number, an optional page number, and an optional reply command with a reply value.
I currently implement the command method like this (The attributes above the method are for help menu information. The HELP command uses reflection to read all these and display an organized help menu):
/// <summary>
/// Shows a topic and all replies to that topic.
/// </summary>
/// <param name="args">A string list of user-supplied arguments.</param>
[CommandInfo("Displays a topic and its replies.")]
[CommandArgInfo(Name="ID", Description="Specify topic ID to display the topic and all associated replies.", RequiredArgument=true)]
[CommandArgInfo(Name="REPLY \"reply\"", Description="Subcommands can be used to navigate pages, reply to the topic, edit topic or a reply, or delete topic or a reply.", RequiredArgument=false)]
public void TOPIC(List<string> args)
{
if ((args.Count == 1) && (args[0].IsInt64()))
TOPIC_Execute(args); // View the topic.
else if ((args.Count == 2) && (args[0].IsInt64()))
if (args[1].ToLower() == "reply")
TOPIC_ReplyPrompt(args); // Prompt user to input reply content.
else
_result.DisplayArray.Add("Subcommand Not Found");
else if ((args.Count >= 3) && (args[0].IsInt64()))
if (args[1].ToLower() == "reply")
TOPIC_ReplyExecute(args); // Post user's reply to the topic.
else
_result.DisplayArray.Add("Subcommand Not Found");
else
_result.DisplayArray.Add("Subcommand Not Found");
}
Run Code Online (Sandbox Code Playgroud)
My current implementation is a huge mess. I wanted to avoid giant IF statements, but all I did was trade one giant IF statement for all the commands, for a ton of slightly less giant IF statements for every command and its arguments. This isn't even the half of it; I simplified this command for this question. In actual implementation there are quite a few more arguments that can be provided with this command and that IF statement is the ugliest thing I have ever seen. It's very redundant and not at all DRY (don't repeat yourself) as I have to display "Subcommand Not Found" in three different places.
Suffice it to say, I need a better solution than this.
Ideally I would love to structure my command methods something like his:
public void TOPIC(int Id, int? page)
{
// Display topic to user, at specific page number if supplied.
}
public void TOPIC(int Id, string reply)
{
if (reply == null)
{
// prompt user for reply text.
}
else
{
// Add reply to topic.
}
}
Run Code Online (Sandbox Code Playgroud)
Then I'd love to do this:
InvokeCommand() on ICommandModule.InvokeCommand() performs some magic parsing and reflection to choose the right command method with the right arguments and invokes that method, passing in only the necessary arguments.I'm not sure how to structure this logic. I've been scratching my head for days. I wish I had a second pair of eyes to help me out on this (hence finally resorting to a novel of an SO question). In what order should things happen?
Should I pull out the command, find all methods with that command name, then loop through all the possible arguments, then loop through my command string's arguments? How do I determine what goes where and what arguments go in pairs. For instance, if I loop through my command string and find Reply "reply" how do I pair the reply content with the reply variable, while encountering <ID> number and supplying it for the Id argument?
I'm sure I'm confusing the hell out of you now. I'm confusing the hell out of me. Let me illustrate with some examples of command strings the user might pass in:
TOPIC 36 reply // Should prompt the user to enter reply text.
TOPIC 36 reply "Hey guys what's up?" // Should post a reply to the topic.
TOPIC 36 // Should display page 1 of the topic.
TOPIC 36 page 4 // Should display page 4 of the topic.
Run Code Online (Sandbox Code Playgroud)
How do I know to send 36 to the Id parameter?
How do I know to pair reply with "Hey guys what's up?" and pass "Hey guys what's up?" as the value for the reply argument on the method?
In order to know which method overload to call I need to know how many arguments where supplied so that I can match that number to the overload of the command method that takes that same number of arguments. The problem is, `TOPIC 36 reply "Hey guys what's up?" is actually two arguments, not three as reply and "Hey guys..." go together as one argument.
I don't mind bloating the InvokeCommand() method a little (or a lot) as long as it means that all the complex parsing and reflection nonsense is handled there and my command methods can remain nice and clean and easy to write.
I guess I'm really just looking for some insight here. Does anyone have any creative ideas to solve this problem? It really is a big issue because the argument IF statements are currently making it very complicated to write new commands for the application. The commands are the one part of the application that I want to be super simple so that they can be easily extended and updated. Here is what the actual TOPIC command method looks like in my app:
/// <summary>
/// Shows a topic and all replies to that topic.
/// </summary>
/// <param name="args">A string list of user-supplied arguments.</param>
[CommandInfo("Displays a topic and its replies.")]
[CommandArgInfo("ID", "Specify topic ID to display the topic and all associated replies.", true, 0)]
[CommandArgInfo("Page#/REPLY/EDIT/DELETE [Reply ID]", "Subcommands can be used to navigate pages, reply to the topic, edit topic or a reply, or delete topic or a reply.", false, 1)]
public void TOPIC(List<string> args)
{
if ((args.Count == 1) && (args[0].IsLong()))
TOPIC_Execute(args);
else if ((args.Count == 2) && (args[0].IsLong()))
if (args[1].ToLower() == "reply" || args[1].ToLower() == "modreply")
TOPIC_ReplyPrompt(args);
else if (args[1].ToLower() == "edit")
TOPIC_EditPrompt(args);
else if (args[1].ToLower() == "delete")
TOPIC_DeletePrompt(args);
else
TOPIC_Execute(args);
else if ((args.Count == 3) && (args[0].IsLong()))
if ((args[1].ToLower() == "edit") && (args[2].IsLong()))
TOPIC_EditReplyPrompt(args);
else if ((args[1].ToLower() == "delete") && (args[2].IsLong()))
TOPIC_DeleteReply(args);
else if (args[1].ToLower() == "edit")
TOPIC_EditExecute(args);
else if (args[1].ToLower() == "reply" || args[1].ToLower() == "modreply")
TOPIC_ReplyExecute(args);
else if (args[1].ToLower() == "delete")
TOPIC_DeleteExecute(args);
else
_result.DisplayArray.Add(DisplayObject.InvalidArguments);
else if ((args.Count >= 3) && (args[0].IsLong()))
if (args[1].ToLower() == "reply" || args[1].ToLower() == "modreply")
TOPIC_ReplyExecute(args);
else if ((args[1].ToLower() == "edit") && (args[2].IsLong()))
TOPIC_EditReplyExecute(args);
else if (args[1].ToLower() == "edit")
TOPIC_EditExecute(args);
else
_result.DisplayArray.Add(DisplayObject.InvalidArguments);
else
_result.DisplayArray.Add(DisplayObject.InvalidArguments);
}
Run Code Online (Sandbox Code Playgroud)
Isn't that ridiculous? Every command has a monster like this and it's unacceptable, but I'm at the end of my rope. I am hurting my brain sitting here staring for hours on end just going over scenarios in my head and how code might handle it. I was pretty proud of my command module setup, now if I could just be proud of the command method implementation...
Unless I get some really brilliant suggestions right off the bat then I plan to keep this question unanswered for a while so that I can get a fair amount of ideas.
While I'm not looking to jump ship with my entire model (command modules) for the application, I am definitely open to suggestions. I'm mostly interested in suggestions related to parsing the command line string and mapping its arguments to the right method overloads. I'm sure whatever solution I go with will require a fair amount of redesign so don't be afraid to suggest anything you think is valuable, even if I don't necessarily use your suggestion it may put me on the right track.
I just wanted to clarify real quick that the mapping of commands to command methods is not really something I'm worried about. I'm mostly concerned about how to parse and organize the command line string. Currently the InvokeCommand() method employs some very simple C# reflection to find the appropriate methods:
/// <summary>
/// Invokes the specified command method and passes it a list of user-supplied arguments.
/// </summary>
/// <param name="command">The name of the command to be executed.</param>
/// <param name="args">A string list of user-supplied arguments.</param>
/// <param name="commandContext">The current command context.</param>
/// <param name="controller">The current controller.</param>
/// <returns>The modified result object to be sent to the client.</returns>
public object InvokeCommand(string command, List<string> args, CommandContext commandContext, Controller controller)
{
_result.CurrentContext = commandContext;
_controller = controller;
MethodInfo commandModuleMethods = this.GetType().GetMethod(command.ToUpper());
if (commandModuleMethods != null)
{
commandModuleMethods.Invoke(this, new object[] { args });
return _result;
}
else
return null;
}
Run Code Online (Sandbox Code Playgroud)
So as you can see, I'm not worried about how to find the command methods as that is already working. I'm just pondering a good way to parse the command string, organize arguments, and then using that information to pick the right command method/overload using reflection.
I have started a bounty on this question. I am looking for a really good way to parse the command string I'm passing in. I want the parser to identify several things:
I want these to be identified via metadata on the first command method overload. Here is a list of sample methods I want to write, decorated with some metadata to be used by the parser when it is doing reflection. I will give you these method samples and some sample command strings that should map to that method. Then I will leave it up to you fine SO folk to come up with a good parser solution.
// Metadata to be used by the HELP command when displaying HELP menu, and by the
// command string parser when deciding what types of arguments to look for in the
// string. I want to place these above the first overload of a command method.
// I don't want to do an attribute on each argument as some arguments get passed
// into multiple overloads, so instead the attribute just has a name property
// that is set to the name of the argument. Same name the user should type as well
// when supplying a name/value pair argument (e.g. Page 3).
[CommandInfo("Test command tests things.")]
[ArgInfo(
Name="ID",
Description="The ID of the topic.",
ArgType=ArgType.ValueOnly,
Optional=false
)]
[ArgInfo(
Name="PAGE",
Description="The page number of the topic.",
ArgType=ArgType.NameValuePair,
Optional=true
)]
[ArgInfo(
Name="REPLY",
Description="Context shortcut to execute a reply.",
ArgType=ArgType.NameValuePair,
Optional=true
)]
[ArgInfo(
Name="OPTIONS",
Description="One or more options.",
ArgType=ArgType.MultiOption,
Optional=true
PossibleValues=
{
{ "-S", "Sort by page" },
{ "-R", "Refresh page" },
{ "-F", "Follow topic." }
}
)]
[ArgInfo(
Name="SUBCOMMAND",
Description="One of several possible subcommands.",
ArgType=ArgType.SingleOption,
Optional=true
PossibleValues=
{
{ "NEXT", "Advance current page by one." },
{ "PREV", "Go back a page." },
{ "FIRST", "Go to first page." },
{ "LAST", "Go to last page." }
}
)]
public void TOPIC(int id)
{
// Example Command String: "TOPIC 13"
}
public void TOPIC(int id, int page)
{
// Example Command String: "TOPIC 13 page 2"
}
public void TOPIC(int id, string reply)
{
// Example Command String: TOPIC 13 reply "reply"
// Just a shortcut argument to another command.
// Executes actual reply command.
REPLY(id, reply, { "-T" });
}
public void TOPIC(int id, List<string> options)
{
// options collection should contain a list of supplied options
Example Command String: "TOPIC 13 -S",
"TOPIC 13 -S -R",
"TOPIC 13 -R -S -F",
etc...
}
Run Code Online (Sandbox Code Playgroud)
The parser must take in a command string, use reflection to find all possible command method overloads, use reflection to read the argument attributes to help determine how to divide up the string into a proper list of arguments, then invoke the proper command method overload, passing in the proper arguments.
Let me know if you need any clarification.
Any help is appreciated. Thanks everyone!
PS - If you're interested at all in taking a peek at the source code for the application, it is open source and located at http://u413.googlecode.com. Currently I am the only developer but if you find yourself interested in the project then drop me a line as per the instructions on the project page.
看看Mono.Options.它目前是Mono框架的一部分,但可以下载并用作单个库.
出于某种原因,它曾经被托管的网站已关闭,但您可以将Mono中使用的当前版本作为单个文件获取.
string data = null;
bool help = false;
int verbose = 0;
var p = new OptionSet () {
{ "file=", v => data = v },
{ "v|verbose", v => { ++verbose } },
{ "h|?|help", v => help = v != null },
};
List<string> extra = p.Parse (args);
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1822 次 |
| 最近记录: |