Archivo

Archive for the ‘Resolución de problemas’ Category

An operations error occurred con Directorio Activo


Si me has seguido últimamente sabrás que he estado trabajando con el Directorio Activo. Primero escribí sobre cómo realizar búsquedas de usuarios en el AD. Luego, hablé sobre un problema de acceso denegado al actualizar propiedades del AD. Pues bien, ahora nos hemos topado con otro problema.

Resulta que al ejecutar el código mostrado en el artículo anterior todo funciona bien. Hasta que metimos dicho código en una página de aplicación de SharePoint. Cuál no sería nuestra sorpresa al encontrarnos con un System.DirectoryServices.DirectoryServicesCOMException con el siguiente mensaje de error: “an operations error occurred”. Sin mayor información.

La frustración fue mayor cuando vimos que el error ocurría al realizar la búsqueda de usuario, ni siquiera al actualizar algún dato.

auto searcher = gcnew DirectorySearcher();            
searcher->Filter = String::Format(L"(SAMAccountName={0})", account);            
            
auto result = searcher->FindOne(); // <-- An operations error occurred

Pues menudo lío. Comenzamos a hacer una búsqueda en Internet, con varias recomendaciones. En particular dimos con esta: Common System.DirectoryServices Issues and Solution. Intentamos seguir las recomendaciones expuestas en la tercera pregunta, con estos resultados.

1.- Usar acceso anónimo. No podemos porque todo el portal de SharePoint no usa acceso anónimo, y por reglas de seguridad del cliente no podemos emplearlo.

2.- Utilizar un componente COM+. ¿En serio? ¿Quién emplea COM+ hoy en día? Además instalar este tipo de componentes nos hubiera llevado días, por políticas del cliente.

3.- Delegación mediante <identity impersonate=”true”> en el web.config. Lo intentamos, pero no nos funcionó.

4.- Correr el proceso del IIS como usuario de dominio. El cliente casi nos corre de su oficina. Y no podemos usar al usuario que ya nos había dado porque pondría en riesgo toda la granja ya existente.

5.- Personificar programáticamente al usuario. Lo intentamos, pero ¿cómo?

Es decir, 5) parecía el camino a seguir. Intentamos correr SPSecurity.RunWithElevatedPrivileges sin suerte. El problema es que el usuario y la contraseña los ponemos después de encontrar al usuario, cuando queremos modificar. ¿Entonces?

DirectorySearcher tiene un parámetro en su constructor: un DirectoryEntry (al cual podríamos poner un usuario y una contraseña), el cual usualmente tiene la unidad organizacional de la raíz del dominio. El problema en mi caso es que mi cliente tiene un bosque con ocho dominios, y ciertamente quiere que buscásemos en cada uno de estos al usuario, porque cambian constantemente. De ahí que DirectorySearcher siempre lo iniciábamos con el constructor vacío.

Fue ahí cuando se me ocurrió: ¿y si creo un DirectoryEntry vacío, le pongo un usuario y contraseña, y lo paso al DirectorySearcher?

auto entry = gcnew DirectoryEntry();
entry->Username = "arthur.dent";
entry->Password = "42";

auto searcher = gcnew DirectorySearcher(entry);
searcher->Filter = String::Format(L"(SAMAccountName={0})", account);

auto result = searcher->FindOne();

 

Y para mi sorpresa, el código anterior funcionó. Nota que el DirectoryEntry, en lugar de pasarle el dominio o alguna OU, lo dejamos vacío y sólo le pusimos las credenciales. Luego, creamos el searcher con éstas, y buscamos al usuario. Y vaya: nos trajo todo completito. Y ahora ya está funcionando esta parte.

¡Espero que esto sirva de ayuda!

Acceso denegado al actualizar propiedades del directorio activo


Hace poco publiqué una entrada sobre un pequeño código para leer un usuario en un directorio activo. También comentaba que estuve apoyando a un colega con esto. Continuando con la historia, resulta que estuvimos trabajando para no solo leer el código, sino para modificarlo. Para hacer eso, utilizamos un código similar al siguiente.

try
{
    auto searcher = gcnew DirectorySearcher();            
    searcher->Filter = String::Format(L"(SAMAccountName={0})", account);
    searcher->PropertiesToLoad->Add(L"displayName");
    searcher->PropertiesToLoad->Add(L"givenname");
    searcher->PropertiesToLoad->Add(L"sn");
    searcher->PropertiesToLoad->Add(L"company");
            
    auto result = searcher->FindOne();
    if (result == nullptr)
        throw gcnew Exception(L"Usuario no encontrado.");

    auto entry = result->GetDirectoryEntry();
    entry->Properties[L"displayName"]->Value = L"Arthur Dent";
    entry->Properties[L"givenname"]->Value = L"Arthur";
    entry->Properties[L"sn"]->Value = L"Dent";
    entry->Properties[L"company"]->Value = L"Heart of Gold";
    entry->CommitChanges();
}
catch (Exception^ ex)
{
    Console::WriteLine(ex->Message);
}

En este código buscamos un usuario por su SAM AccountName, cargamos algunas propiedades y luego las modificamos. Queremos buscar a un usuario y asignarle sus nombres y la compañía en la que trabaja. Cualquiera que haya leído la documentación de MSDN puede llegar a un código como el anterior: la colección “Properties” contiene las propiedades en el directorio activo. Y el método CommitChanges hace la actualización hacia el AD.

Bien. Hasta aquí pensábamos que la vida era rosa. Ejecutamos el código y ¡BOOM! excepción: SecurityException, Access Denied. Que no panda el cúnico, nos dijimos. Seguramente es de esperar que no cualquier fulano pueda modificar al AD. Entonces probamos ejecutar la anterior aplicación con permisos de administrador.

Nada. Bueno, quizás el administrador que utilizábamos no tiene permisos de modificar el AD. Le hablamos a nuestro cliente y le pedimos que nos generara un usuario con permisos para modificar el dichoso AD.

 

try
{
    auto searcher = gcnew DirectorySearcher();            
    searcher->Filter = String::Format(L"(SAMAccountName={0})", account);
    searcher->PropertiesToLoad->Add(L"displayName");
    searcher->PropertiesToLoad->Add(L"givenname");
    searcher->PropertiesToLoad->Add(L"sn");
    searcher->PropertiesToLoad->Add(L"company");
            
    auto result = searcher->FindOne();
    if (result == nullptr)
        throw gcnew Exception(L"Usuario no encontrado.");

    auto entry = result->GetDirectoryEntry();
    entry->Username = L"my.admin";
    entry->Password = L"42";
    entry->Properties[L"displayName"]->Value = L"Arthur Dent";
    entry->Properties[L"givenname"]->Value = L"Arthur";
    entry->Properties[L"sn"]->Value = L"Dent";
    entry->Properties[L"company"]->Value = L"Heart of Gold";
    entry->CommitChanges();
}
catch (Exception^ ex)
{
    Console::WriteLine(ex->Message);
}

Como puedes ver, el código cambió un poco. Las líneas resaltadas muestran cómo pusimos el nombre de usuario y la contrasña del usuario que nos generó el cliente, con permisos para modificar el AD. Corremos el programa, y… ¡tan tan taaan! Acceso denegado.

Estuvimos explorando varias posibilidades. Hablamos con el cliente, le dijimos que seguía dando el error. Le solicitamos que pusiera al usuario que nos generó dentro del grupo de Enterprise Administratos. A lo que se negó, por supuesto. Nos explicó que lo que hizo fue delegar los permisos para que pudiera modificar propiedades, y que ya se había propagado el cambio en todo el bosque.

Frustrados, intentamos buscar más explicaciones. Hasta que en una reunión con la gente de AD del cliente, se pusieron a revisar y se dieron cuenta de que los usuarios que intentábamos modificar tenían la opción marcada para que no se replicara la delegación de permisos. Jope: eran las cuentas destino las que no podían modificarse. Bastó unos cuantos minutos para que cambiaran en el AD a esas cuentas, y todo funcionó a la perfección.

Así que, en resumen, si obtienes un acceso denegado, revisa:

1.- Impersonar con una cuenta que sí tenga permisos para modificar el AD.

2.- Que las cuentas destino tengan permisos para ser modificadas.

3.- En caso de que deleguen permisos, que las cuentas destino acepten la propagación de delegación.

4.- Si no funciona lo anterior, que el usuario ingrese al grupo de Enterprise Administrators y búscale a partir de ahí.

¡Saludos!

El método SelectNodes regresa una lista vacía tras una consulta con XPath


Ando haciendo un programa para descargar perfiles de usuarios en SharePoint (el cual espero publicar pronto en la galería de código de MSDN). Uno de los requerimientos del programa consiste en usar un WebService y descargar un XML como el siguiente:

<GetUserCollectionFromWeb xmlns="http://schemas.microsoft.com/sharepoint/soap/directory/">
<Users>
 <User ID="190" LoginName="dominio\nombre1" Email="nombre1@dominio.com.mx" />
 <User ID="111" LoginName="dominio\nombre2" Email="nombre2@dominio.com.mx" />
 </Users>
 </GetUserCollectionFromWeb>

Así pues, parte de la lógica consiste en iterar sobre cada elemento de <Users> y mostrarlo en una cuadrícula (GridView) de Windows Forms. Easy peasy, dado que el servicio web me regresa un XML, se me ocurre hacerlo con XPath. Este es mi código inicial.

XmlNode mainNode = webService.GetUserCollectionFromWeb();

string xpath = "GetUserCollectionFromWeb/Users/descendant::User";
XmlNodeList userNodes = mainNode.SelectNodes(xpath);
foreach (XmlNode userNode in userNodes)
{
    string loginName = userNode.Attributes["LoginName"].Value;
    // hacer algo con loginName;
}

Sin embargo, al hacer la prueba, el método SelectNodes me regresaba 0 elementos. Mala cosa, digo yo. Estuve revisando un rato, hasta que me decidí a hacer una pregunta en el foro de C# de MSDN. Y como siempre, obtuve apoyo para resolver el problema.

En primer lugar, he de decir, me apoyaron con la consulta XPath, ya que yo la tenía diferente. Aún con ese cambio, SelectNodes se negaba a cooperar. Fue entonces que encontramos la respuesta: el XML en cuestión tiene un espacio de nombres por defecto, xmlns, sobrescrito. Luego, hay que añadir un XmlNamespaceManager que lo contenga. Pero para crear una instancia de dicha clase, hay que pasarle al constructor un XmlNameTable, y la forma más sencilla es mediante un XmlDocument. Una vez creado, hay que añadir el espacio de nombres en cuestión y ponerle un prefijo cualquiera (en este caso, opté por “defns”). Acto seguido, modificamos el XPath y prefijamos los elementos (en mi caso, por ejemplo, cambiamos Users por defns:Users, y así). Y por último, llamamos a SelectNodes y pasamos como segundo parámetro el XmlNamespaceManager creado con anterioridad.

Y así quedó el código:

XmlNode mainNode = webService.GetUserCollectionFromWeb();

XmlDocument doc = new XmlDocument();
doc.LoadXml(mainNode.OuterXml);
XmlNamespaceManager nsmgr = new XmlNamespaceManager(mainNode.OwnerDocument.NameTable);
nsmgr.AddNamespace("defns", "http://schemas.microsoft.com/sharepoint/soap/directory/");

XmlNodeList userNodes = doc.SelectNodes("defns:GetUserCollectionFromWeb/defns:Users/descendant::defns:User", nsmgr);
foreach (XmlNode userNode in userNodes)
{
    string loginName = userNode.Attributes["LoginName"].Value;
    // hacer algo con loginName;
}

¡Y voilá! Muchas gracias a Pedro Hurtado por el apoyo, y como siempre, a Leandro Tuttini por sus valiosas contribuciones.

Categorías:.NET Framework, C#, Resolución de problemas Etiquetas: ,

Problemas al mostrar ventana emergente


Hace unos días escribí sobre cómo mostrar una ventana emergente en SharePoint. Pues bien, todo se reducía a un simple código con JavaScript, el cual reproduzco:

<script type="text/javascript" language="javascript">
    function openAsgardDialog()
    {
        var options = {
            url: "/Pages/AsgardPantheon.aspx",
            width: 800,
            height: 650,
            title: "Hola mundo",
	    };
	    SP.UI.ModalDialog.showModalDialog(options);
    }
</script>";

Ahora bien, ese código funciona bien… o eso pensé. Resulta que correr eso en el Internet Explorer 7 nos piña y nos manda un error, diciendo que no se pudieron cargar los controles Ajax. El hecho, sin embargo, es que esta sintaxis no le funciona, al parecer. Estuve investigando y aquí hay una sintaxis alternativa para iniciar el objeto. Por lo demás, queda igual…

<script type="text/javascript" language="javascript">
    function openAsgardDialog() 
    {
        var options = SP.UI.$create_DialogOptions();
        options.url = "/Pages/AsgardPantheon.aspx";
        options.height = 800;
        options.width = 650;
        options.title = "Hola mundo";

	    SP.UI.ModalDialog.showModalDialog(options);
    }
</script>

Una sintaxis un poquito más tradicional, pero funciona con IE 7.

Depurando un sitio SharePoint à la ASP.NET


Cuando desarrollamos nuestros sitios en SharePoint, a veces cometemos errores y una excepción se lanza. Cuando esto ocurre, SharePoint atrapa la excepción y muestra una página, la cual dice que ha ocurrido un error. Esto es, simplemente, inútil. En ocasiones, necesitamos algo más jugoso que nos diga lo que en realidad aconteció.

El motor de ejecución de ASP.NET muestra una página, famosa, cuando algo así ocurre: con fondo blanco y el mensaje de error en grandes letras rojas, además de mostrar información de depuración, como el rastreo de la pila, en un fondo amarilloso. Ésta sí nos puede dar mayor detalle de lo ocurrido.

Cualquier programador ASP.NET sabe, por supuesto, que el cliente nunca debería ver esta página. Lo normal es que se atrape la excepción, o bien, se diriga al usuario a una página que muestre el menaje de error, posiblemente con la ayuda sobre cómo resolverlo. Pero la página de error de SharePoint es basura pura: no nos sirve. Así, durante nuestro período de desarrollo, y durante las pruebas antes de entregar nuestro proyecto, es posible que queramos ver la buena página de error de ASP.NET.

Bueno, pues para ello, tenemos que hacer tres cosas sencillas.

1.- Abrimos el archivo web.config de nuestro sitio. Siempre es buena idea hacer un respaldo del web.config, por si la regamos.

2.- Buscamos la etiqueta <SharePoint>, dentro de la cual debe existir etiqueta llamada <SafeMode> con un atributo, CallStack, igual a false. Cambiémosla por true.  Algo así debe lucir:

<SharePoint>
    <SafeMode CallStack="true" />
</SharePoint>

3.- Luego, buscamos la etiqueta <system.web>, dentro de la cual existe otra etiqueta llamada <customErrors> con un atributo mode igual a On. Cambiemos dicho valor por Off.

4.- Existe otra etiqueta, igualmente dentro de <system.web>, llamada <compilation> con un atributo debut igual a false. Cambiémosa por true.

<system.web>
    <customErrors mode="Off" />
    <compilation debug="true" />
</system.web>

5.- Guardamos los cambios y estamos listos. Si tarda un poco en tomar efecto, reinicia el IIS mediante un “iisreset /noforce” en la línea de comandos.

Consejo final: no olvides regresar a los valores originales antes de que el sitio sea promovido a un ambiente de producción.

Saludos.

Error 9002 después de un intento por encoger la base de datos


El día de hoy ha sido terrible. Resulta que el proyecto de GeoSEP que tengo está en un servidor de desarrollo compartido. Pero ya no hay espacio en disco duro. Me meto al servidor y menudo lío: solo 9MB de espacio disponible. Ni pex, tiempo de liberar espacio.

Entro a la dirección de SQL Server ($SQLSERVER\MSSQL10.MSSQLSERVER\MSSQL\DATA) y veo que mi base de datos tiene un log de 500MB. Pero además hay una base de datos cuyo MDF mide 3GB y su archivo log LDF mide 2GB! Así que aquí está el pan. Basta con hacer una reducción de ambas bases de datos para ganar 2.5GB nuevamente.

Así pues, abro el Management Studio y expando las bases de datos. Selecciono la mía y hago clic secundario y selecciono “Tasks->Shrink->Database”. Se ejecuta la transacción y listo, 500GB menos. Perfecto. Hago lo propio con la otra base de datos… ¡Y patatús! Resulta que en lugar de reducirse el archivo LDF, ¡éste comienza a aumentar de tamaño! de 2GB pasó a 2.5GB en unos segundos… y lanza error porque ya no hay espacio en disco duro. Me da el patatús.

¿De quién cuernos es esa base de datos? Investigo, y resulta que no es de nadie, ya no se usa. Pero no se puede desaparecer, hay que respaldarla. Bien. Voy al server de nuevo e intento hacerle un “detach” a la base de datos… y me lanza el siguiente error.

The transaction log for database ‘xxx’ is full. To find out why space in the log cannot be reused, see the log_reuse_wait

Me dá el patatús. Ni jota idea del por qué. Pero el mensaje dice que revise log_reuse_wait de la tabla sys.databases, así que hay voy. El campo log_reuse_wait_desc tiene el valor ‘LOG_BACKUP’, que no dice absolutamente nada. Vale, vamos a la documentación a buscar algo. Encuentro un artículo en MSDN que no da información útil.

Así que regreso a la base de datos. Intento hacer un respaldo y me sale el mismo error. Intento ver las propiedades, error. Intento hacer cualquier cosa, y error. Comienzo a perder la paciencia, así que salgo a comer para despejarme.

Regresando, a continuar. Comienzo a buscar en Google y los foros de MSDN, sin tener mucho resultado. Algunas soluciones proponen que se haga un detach/attach, pero es precisamente lo que no puedo hacer. Intento también volver a correr el comando para reducir la base de datos, y el mismo error.

Abro mi explorador y navego a la página de foros de MSDN. Localizo el foro para SQL Server, a ver si alguien me puede ayudar. Comienzo a redactar la pregunta, y cuando regreso al servidor para copiar el mensaje de error, noto algo que no había visto: “Microsoft SQL Server Error 9002”. ¡Ah, hay un código de error! Abro Google y a buscar. Y finalmente encontré una solución.

1. Ejecutar el procedimiento almacenado sp_resetstatus, así:

exec sp_resetstatus 'mi_basededatos'

2. Ejecutar el comando para recuperar una base de datos:

 DBCC DBRECOVER ('mi_basededatos')

Intento correr el detach… ¡y ahora sí funciona! ¡Uf! Me recargo en la pared, supongo que no ganaré el premio al mejor DBA del año. Pero bueno, ya quedó. Dejo un par de enlaces que me dieron la solución, por si alguien más con este problema los quiere leer.

Bueno, ahora sí, de vuelta al trabajo.

Mi fuente de datos no regresa valores con parámetros nulos


Me recargo en la pared. Benditos sean los dioses. Hoy viernes, día que deberíamos consagrar exclusivamente a Freyja (Friday, Freitag, etc.), tomando cerveza y comiendo carne asada de venado, me he topado con un problema que me ha impedido celebrar como se debe a la adorada diosa.

Pues resulta que en el trabajo, ando creando una aplicación web de geo-codificación. En esencia, la aplicación lee una base de datos de la Secretaría de Educación Pública y obtiene todas las escuelas registradas, se aplican algunos filtros según el deseo del usuario, para luego mostrarlas en un mapa de Google. Pan comido (y de muerto, ahora que comienza la temporada).

Así las cosas, el primer paso es realizar el filtro. Mi procedimiento almacenado es sencillo: un vil select con unas validaciones y listo.

create procedure SchoolSearch
    @Key nvarchar(12) = null,
    @StateId nvarchar(4) = null,
    @MunicipalityId nvarchar(5) = null,
    @Name nvarchar(82) = null
as
begin

    if (isnull(@Name, '') = '') begin set @Name = '%' end
    else begin set @Name = '%' + rtrim(@Name) + '%' end
    if (rtrim(isnull(@Key, '')) = '') begin set @Key = '%%' end
    else begin set @Key = '%' + rtrim(@Key) + '%' end
    if (isnull(@StateId, '') = '') begin set @StateId = '%' end
    else begin set @StateId = '%' + rtrim(@StateId) + '%' end
    if (isnull(@MunicipalityId, '') = '') begin set @MunicipalityId = '%' end
    else begin set @MunicipalityId = '%' + rtrim(@MunicipalityId) + '%' end

    select CCT, NOM_CCT, NOMBRETUR, TIPO, NIVEL, X, Y , ENTIDAD, NOMBREMUN
    from Escuelas
    where
        CCT like @Key
        and ENT like @StateId
        and MUNICIPIO like @MunicipalityId
        and NOM_CCT like @Name
    order by ENTIDAD, NOMBREMUN

end

¡Pffft! Papita. Segunda parte, en mi página ASP.NET, creo una vil fuente de datos usando SqlDataSource. Algo así.

<asp:SqlDataSource ID="_searchDataSource" runat="server" ConnectionString="<%$ ConnectionStrings:GeoSepAnonymousDatabase %>"
    SelectCommand="SchoolSearch" SelectCommandType="StoredProcedure">
    <SelectParameters>
        <asp:Parameter Name="Key" DbType="String" Size="12" ConvertEmptyStringToNull="false"   />
        <asp:Parameter Name="StateId" DbType="String" Size="4" ConvertEmptyStringToNull="true" />
        <asp:Parameter Name="MunicipalityId" DbType="String" Size="5" ConvertEmptyStringToNull="true" />
        <asp:Parameter Name="Name" DbType="String" Size="82" ConvertEmptyStringToNull="true" />
    </SelectParameters>
</asp:SqlDataSource>

¡Pffft! Kindergarten. Tercera parte, un poquito de código C# que actualiza la fuente de datos en base a los filtros seleccionados en otros controles (_stateList y _municipalityList son DropDownList, para finalmente hacer el DataBind sobre el GridView (_catalogueView) y mostrar todo en una ventana modal (ModalPopupExtender) del Ajax Control Toolkit (_showCataloguePopup).

protected void HandleSearchClick(object sender, EventArgs args)
{
    if (!string.IsNullOrEmpty(_stateList.SelectedValue))
        _searchDataSource.SelectParameters["StateId"].DefaultValue =
                 _stateList.SelectedValue;
    else
        _searchDataSource.SelectParameters["StateId"].DefaultValue =
                 string.Empty;

    if (!string.IsNullOrEmpty(_municipalityList.SelectedValue))
        _searchDataSource.SelectParameters["MunicipalityId"].DefaultValue =
                 _municipalityList.SelectedValue;
    else
        _searchDataSource.SelectParameters["MunicipalityId"].DefaultValue =
                 string.Empty;

    _catalogueView.DataBind();
    _showCataloguePopup.Show();
}

¡Pfft! Piece of… Scheisse! Pues sí, usando ASP.NET Framework 3.5 y C# 3, con SQL Server 2008, lo anterior no jala. Es decir, si yo selecciono un valor, me despliega los resultados como debería. Perfecto. Pero si no selecciono nada (es decir, si paso al procedimiento almacenado un valor nulo), no me regresa registro alguno.

No tiene sentido. Abro el SQL Server Management Studio, y corro el procedimiento con los cuatro parámetros en nulos. y funciona, como debe de ser. Luego, para probar que no estoy loco, me creo un SqlConnection, un SqlCommand, un SqlDataAdapter y a partir de éstos, relleno un DataSet: éste devuelve datos. Incluso si hago el enlace entre el GridView y el DataSet, todo es maravilloso. Pero no tiene sentido que no funcione el SqlDataSource. ¿Qué puede estar pasando?

Bueno, pues esto me llevó un ratillo hasta que di en el clavo. Para esto, abrí el Profiler de SQL Server 2008 y vi que no se ejecutaba ninguna consulta. Extrañado (y en este punto, ya algo molesto con el mundo) Comencé a poner mensajes y hacer rastreos, quitando propiedades y así. El problema tenía que estar en el SqlDataSource, porque demonios, todo lo demás parecía funcionar. Así que me puse a revisar todas las propiedades de este objeto. Y di con una que me llamó la atención: CanSelectOnNullParameter.

El nombre me resultó demasiado sospechoso como para ignorarlo. Así que cambié la definición de mi fuente de datos e incorporé semejante parámetro, estableciéndolo a false.

<asp:SqlDataSource ID="_searchDataSource" runat="server"
    ConnectionString="<%$ ConnectionStrings:GeoSepAnonymousDatabase %>"
    SelectCommand="SchoolSearch" SelectCommandType="StoredProcedure"
    CancelSelectOnNullParameter="false">
    <SelectParameters>
        <asp:Parameter Name="Key" DbType="String" Size="12" ConvertEmptyStringToNull="false"   />
        <asp:Parameter Name="StateId" DbType="String" Size="4" ConvertEmptyStringToNull="true" />
        <asp:Parameter Name="MunicipalityId" DbType="String" Size="5" ConvertEmptyStringToNull="true" />
        <asp:Parameter Name="Name" DbType="String" Size="82" ConvertEmptyStringToNull="true" />
    </SelectParameters>
</asp:SqlDataSource>

 

¡Y funcionó! Una vez que pasó mi anonadamiento me fui derechito a la documentación de MSDN, y encontré esto en la sección de Valor de Propiedad:

Tipo: System.Boolean.- true si se cancela una operación de la recuperación de datos cuando un parámetro contenido en la colección SelectParameters se evalúa como null; de lo contrario, false. El valor predeterminado es true.

¡Me lleva Pifas! Así que eso era: cuando uno tiene parámetros en un select y estos son nulos, el SqlDataSource simplemente decide abortar la operación. Por supuesto, si no es nulo (i.e. un espacio en blanco es suficiente) todo funciona bien. Establecer este valor a falso hace que la vida vuelva a cobrar sentido, ya que con esto no interrumpe nada y le pasa el valor nulo al procedimiento almacenado, el cual arreglará cualquier valor posible.

Así que bueno, ya no pude tomar cerveza y comer asado de venado por arreglar esto. Pero ya quedó y todo jala a las mil maravillas. Así que ya es hora de comenzar este fin de semana, y para ello pasaré al mercado por una enorme calabaza para hacerla zumo con lo que acompañaré mi pan de muerto.