2010-11-04 12 views
5

Estoy making a jquery clone para C#. Ahora mismo lo tengo configurado para que cada método sea un método de extensión en IEnumerable<HtmlNode>, por lo que funciona bien con proyectos existentes que ya están usando HtmlAgilityPack. Pensé que podía escapar sin conservar el estado ... sin embargo, noté que jQuery tiene dos métodos .andSelf y .end que "reventan" los elementos coincidentes más recientes de una pila interna. Puedo imitar esta funcionalidad si cambio mi clase para que siempre opere en objetos SharpQuery en lugar de enumerables, pero todavía hay un problema.¿Cómo diseñar mi API C# jQuery de modo que no sea confuso de usar?

Con JavaScript, se le da el documento HTML automáticamente, pero cuando se trabaja en C# tiene que cargarlo explícitamente, y puede usar más de un documento si lo desea. Parece que cuando llamas al $('xxx') esencialmente estás creando un nuevo objeto jQuery y comenzando de nuevo con una pila vacía. En C#, no querrá hacer eso, porque no quiere volver a cargar/volver a buscar el documento desde la web. Entonces, en su lugar, la carga una vez en un objeto SharpQuery o en una lista de HtmlNodes (solo necesita el DocumentNode para comenzar).

En la documentación de jQuery, que dan este ejemplo

$('ul.first').find('.foo') 
    .css('background-color', 'red') 
.end().find('.bar') 
    .css('background-color', 'green') 
.end(); 

no tengo un método de inicialización porque no puedo sobrecargar el operador (), por lo que acaba de empezar con sq.Find() lugar, que opera en el raíz del documento, esencialmente haciendo lo mismo. Pero la gente intentará escribir sq.Find() en una línea, y luego sq.Find() en algún momento, y (con razón) esperan que vuelva a funcionar en la raíz del documento ... pero si mantengo el estado, entonces usted Acabo de modificar el contexto después de la primera llamada.

Entonces ... ¿cómo debo diseñar mi API? ¿Debo agregar otro método Init con el que todas las consultas comiencen con que restablece la pila (pero, entonces, ¿cómo los obligo a comenzar con eso?), O agreguen un Reset() al que tienen que llamar al final de su línea? ¿Sobrecargo el [] y les digo que empiecen con eso? ¿Digo "olvídalo, de todos modos, nadie usa esas funciones conservadas por el estado"?

Básicamente, ¿cómo le gustaría que el ejemplo jQuery se escriba en C#?

  1. sq["ul.first"].Find(".foo") ...
    Desventajas: los abusos de la propiedad [].

  2. sq.Init("ul.first").Find(".foo") ...
    Desventajas: En realidad nada obliga al programador para comenzar con Init, a menos que añado algún mecanismo raro "inicializado"; el usuario puede intentar comenzar con .Find y no obtener el resultado que esperaba. Además, Init y Find son prácticamente idénticos de todos modos, excepto que el primero restablece la pila también.

  3. sq.Find("ul.first").Find(".foo") ... .ClearStack()
    Desventajas: programador puede olvidarse de limpiar la pila.

  4. No se puede hacer.
    end() no implementado.

  5. Utilice dos objetos diferentes.
    Quizás use HtmlDocument como la base con la que todas las consultas deben comenzar, y luego cada método devuelve un objeto SharpQuery que puede encadenarse. De esta manera, el HtmlDocument siempre mantiene el estado inicial, pero los objetos SharpQuery pueden tener estados diferentes. Desafortunadamente, esto significa que tengo que implementar un montón de cosas dos veces (una para HtmlDocument, una para el objeto SharpQuery).

  6. new SharpQuery(sq).Find("ul.first").Find(".foo") ...
    Las copias del constructor una referencia al documento, sino que restablece la pila.

+2

IMO 5) es la mejor opción. Es ciertamente legible e intuitivo que las consultas comiencen desde 'HtmlDocument', y en cuanto al trabajo duplicado, con suerte puede implementarlo de modo que solo las firmas de métodos estén duplicadas, pero no la lógica real. (Encadenando las llamadas a una clase común que hace el trabajo.) También me gusta la opción 1) y personalmente no la veo como un "abuso" de los corchetes. :) –

+0

@Kirk: '[]' generalmente indica una operación 'O (1)' ... como si los datos ya estuvieran indexados. En este caso, en realidad tiene que hacer una búsqueda completa. – mpen

+0

Sin embargo, estoy inclinado hacia (1) ahora. En realidad, comencé a codificarlo de esta manera. Esto evita que los usuarios salten directamente con '.Find()' porque el contexto no está configurado hasta que llamen a '[]'. es decir, '.Find()' simplemente no encontrará nada porque no tiene nada que buscar. '[]' restablece el contexto al nodo del documento y luego pueden comenzar la búsqueda. – mpen

Respuesta

4

Creo que el principal obstáculo con el que se está tropezando aquí es que está tratando de salirse con la suya teniendo solo un objeto SharpQuery para cada documento. Así no es como funciona jQuery; en general, los objetos jQuery son inmutables. Cuando se llama a un método que cambia el conjunto de elementos (como find o end o add), que no altera el objeto existente, pero devuelve una nueva:

var theBody = $('body'); 
// $('body')[0] is the <body> 
theBody.find('div').text('This is a div'); 
// $('body')[0] is still the <body> 

(ver el documentation of end para obtener más información)

SharpQuery debe funcionar de la misma manera. Una vez que crea un objeto SharpQuery con un documento, las llamadas al método deben devolver nuevos objetos SharpQuery, haciendo referencia a un conjunto diferente de elementos del mismo documento. Por ejemplo:

var sq = SharpQuery.Load(new Uri("http://api.jquery.com/category/selectors/")); 
var header = sq.Find("h1"); // doesn't change sq 
var allTheLinks = sq.Find(".title-link") // all .title-link in the whole document; also doesn't change sq 
var someOfTheLinks = header.Find(".title-link"); // just the .title-link in the <h1>; again, doesn't change sq or header 

Las ventajas de este enfoque son varias. Debido a que sq, header, allTheLinks, etc. son todos de la misma clase, solo tiene una implementación de cada método. Sin embargo, cada uno de estos objetos hace referencia al mismo documento, por lo que no tiene múltiples copias de cada nodo y los cambios a los nodos se reflejan en cada objeto SharpQuery en ese documento (por ejemplo, después de allTheLinks.text("foo"), someOfTheLinks.text() == "foo").

Implementando end y las otras manipulaciones basadas en la pila también se vuelven fáciles. A medida que cada método crea un nuevo objeto SharpQuery filtrado de otro, conserva una referencia a ese objeto principal (allTheLinks a header, header a sq). Entonces end es tan simple como el retorno de una nueva SharpQuery que contiene los mismos elementos que los padres, como:

public SharpQuery end() 
{ 
    return new SharpQuery(this.parent.GetAllElements()); 
} 

(o como su sintaxis sacude.)

Creo que este enfoque le dará el más jQuery como el comportamiento, con una implementación bastante fácil.Definitivamente estaré pendiente de este proyecto; es una gran idea.

+0

Ahh..clever! Estaba pensando en devolver un nuevo objeto, pero luego no pude entender cómo iba a mantener la pila. Mantener una referencia al padre ... tan simple>. < – mpen

+0

Gran actualización: http://sharp-query.googlecode.com/svn/trunk/HtmlAgilityPlus/ Utiliza este método para encadenar ahora. – mpen

0

Me inclinaría por una variante en la opción 2. En jQuery $() es una llamada a función. C# no tiene funciones globales, una llamada de función estática es la más cercana. Me gustaría utilizar un método que indica que usted está creando un envoltorio como ..

SharpQuery.Create("ul.first").Find(".foo") 

yo no estaría preocupado por el acortamiento SharpQuery a SQ desde intelisense significa que los usuarios no tendrán que escribir todo el asunto (y si tienen resharper, solo necesitan escribir SQ de todos modos).

+0

Pueden nombrarlo como quieran ... 'sq' sería una instancia de' SharpQuery'. 'SharpQuery.Create' no funcionará como un método estático porque primero debe cargar el documento ... a menos que quiera que el documento sea estático también, pero luego solo puede trabajar con un documento a la vez; una restricción arbitraria. – mpen

Cuestiones relacionadas