A proposito del bug che ho segnalato ieri sera, vorrei rispondere pubblicamente alla domanda che mi ha posto Michele con un commento. In effetti, la prima cosa che ho fatto, subito dopo aver segnalato il palese errore è stato di rivolgermi al Product Feedback Center dove ho trovato naturalmente che il bug era già stato segnalato. Ho provveduto così a informare che anche io ho validato il bug e speriamo che la prossima service pack del framework risolva la cosa. Per il momento però ho trovato un workaround, che però è parecchio laborioso. Portate pazienza, preparo un progettino e poi vi faccio sapere.

powered by IMHO 1.3


Abbiate pazienza, non è mia abitudine lamentarmi, ma stavolta lo sbottare ci sta davvero tutto. Ma prima del fatto è bene che vi racconti l'antefatto. Stasera, io e il collega Andrea Dottor ci siamo scontrati con un comportamento apparentemente inspiegabile. Un pezzo di codice apparentemente perfetto falliva miseramente e inspiegabilmente. In buona sostanza quello che stavamo facendo era tutto sommato semplice; Un campo di tipo Textbox riceveva una stringa in che rappresentava una valuta, per la precisione "0,5" in seguito tale stringa arrivava al validatore serverside che utilizzando decimal.TryParse() verificava che si trattasse effettivamente di un decimal. Questo controllo, applicato sia con la CultureInfo che senza veniva eseguito perfettamente restituendo un chiaro e lampante "true". Tuttavia, pochi millisecondi dopo, quando la datasource tentava di mappare i campi su un oggetto di business, l'operazione miseramente scoppiava, con il seguente errore: "0,5" is not a valid value for Decimal.

Alla fine, solo analizzanto il codice del framework linea per linea con Reflector siamo giunti a capire il motivo per cui questa banale operazione scoppiava:

private static object ConvertType(object value, Type type, string paramName)
{
      
string text1 = value as string;

      
if (text1 != null)
      {
            TypeConverter converter1 = TypeDescriptor.GetConverter(type);

            
if (converter1 == null)
            {
                
return value;
            }
            
try
            
{
                
value = converter1.ConvertFromInvariantString(text1);
            }
            
catch (NotSupportedException)
            {
                
throw new InvalidOperationException(
                    SR.GetString("ObjectDataSourceView_CannotConvertType", 
                        
new object[] { paramName, typeof(string).FullName, type.FullName }));
            }
            
catch (FormatException)
            {
                
throw new InvalidOperationException(
                    SR.GetString("ObjectDataSourceView_CannotConvertType", 
                        
new object[] { paramName, typeof(string).FullName, type.FullName }));
            }
      }
      
return value;
}

Questo codice, estratto dalla classe ObjectDataSourceView è la causa del misfatto e precisamente la riga che chiama il metodo ConvertFromInvariantString() del TypeConverter. La ObjectDataSourceView, per inciso, è l'oggetto che all'interno della ObjectDataSource si incarica di effettuare le operazioni incapsulando al suo interno una rappresentazine dello storage. La chiamata a ConvertFromInvariantString() scritta in questo modo impone che un dato decimal, ma con tutta probabilità anche una data o un double, debba essere formattato con cultura neutra, e cioè nel nostro caso con il punto al posto della virgola. Una chiamata corretta sarebbe stata la seguente:

value = converter1.ConvertFromString(null, CultureInfo.CurrentCulture, text);

In questo modo la datasource avrebbe tenuto conto della cultura corrente e quindi si sarebbe adeguata al contesto del thread, consentendo alla conversione di avvenire correttamente. Ora, purtroppo i metodi in questione sono privati e perciò non è possibile porre rimedio a questo problema cercando di sostituire la ObjectDataSourceView con un oggetto che sia stato patchato. In realtà credo di avere un'idea per una soluzione, che però tenterò domattina, ma lasciatemi dire che un errore del genere non me lo sarei mai aspettato.

powered by IMHO 1.3

 


Su xe.net è apparso il mio secondo articolo che questa volta porta con se un componente riutilizzabile da non perdere. Si tratta di un SiteMapProvider che semplifica enormemente la creazione di provider custom che attingano i dati della mappa da una qualsiasi sorgente. Nell'esempio allegato all'articolo oltre al codice di questo StaticSiteMapProvider che risolve i problemi di concorrenza che affliggono queste implementazioni, anche un semplice provider alimentato da database SqlServer

Link: Uno StaticSiteMapProvider molto flessibile

Con l'occasione ho anche postato il secondo screencast dedicato al pattern singleton

SCREENCAST: Implementare un Sigleton in C#


Continuo sull'argomento System.Configuration, proponendovi un esempio di come creare una collection di ConfigurationElement custom. Per capire l'applicazione dell'esempiuo che sto per introdurre bisogna una ttimo soffermarsi su una particolare tipologia di elementi di configurazione. Mi riferisco ad esempio al nodo <providers> all'interno di una sezione di configurazione di MembershipProvider piuttosto che magari la sezione dedicata ad httpHandler e httpModules. All'interno di questo tipo di sezioni è possibile aggiungere elementi, ma anche rimuoverne con le seguenti sintatti:

<add />

<remove />

<clear />

Nel framework 2.0 questo tipo di sezioni sono perfettamente definibili da parte dell'utente implementando una ConfigurationElementCollection. Si tratta in breve di estendere una classe del framework, implementando un certo numero di metodi: nell'esempio che riporto qui di seguito ecco come realizzare la versione base che accetterà l'aggiunta di nuovi elementi con <add />

public class ApplicationInfoCollection : ConfigurationElementCollection
{
    
protected override ConfigurationElement CreateNewElement()
    {
        
return new ApplicationInfo();
    }

    
protected override object GetElementKey(ConfigurationElement element)
    {
        
return ((ApplicationInfo)element).Name;
    }
}

Con queste poche righe di codice avremo definito una sezione di configurazione che accetta tutte le chiavi che abbiamo elencato poco fa, attribuendo loro la funzionalità che ci aspetteremo. In particolare i due metodi si occupano di creare una nuova istanza di elemento per la collection e di restituire il valore della proprietà considerata chiave all'interno di esso. Questo consente oltre che creare gli elementi con <add /> anche di rimuoverli specificando a quale di essis ci si riferisce. Non rimane ora che esporre una proprietà in una classe:

/// <summary>
/// 
Applicazioni registrate
/// </summary>
[ConfigurationProperty("applications", IsDefaultCollection = false)]
[ConfigurationCollection(
typeof(ApplicationInfo))]
public ApplicationInfoCollection Applications
{
    
get return (ApplicationInfoCollection)this["applications"]; }
    
set this["applications"] = value; }
}

Il primo attributo informa che ci stiamo riferendo ad una proprietà di configurazione che quindi avrà un corrispondente elemento nel file di configurazione. L'elemento in questo caso si chiamerà <applications>. Il secondo attributo invece consente di dire quale tipo di di elemento contiene la collection; Questo in effetti può sembrare ridondante... ma di questo non prendetevela con me.

powered by IMHO 1.3


Se avete provato ad usare i nuovi strumenti per la gestione della configurazione in .NET 2.0, sicuramente ne sarete rimasti affascinati come è successo a me, che oramai cerco di usarli ovunque possibile. Vi sarà forse capitato anche di dover gestire delle configurazioni dinamiche sul tipo di quelle che adottano i vari provider di ASP.NET. Il MembershipProvider ad esempio - ma anche tutti gli altri in realtà - tipicamente espongono una serie di parametri di configurazione che si possono suddividere in parametri "generali", cioè che si applicano indistintamente a tutti i MembershipProvider e in parametri "specifici" che invece variano in base a che tipo di provider dobbiamo configurare. Il SqlMembershipProvider ad esempio richiede una serie di parametri aggiuntivi - uno per tutti, il nome della stringa di connessione - che non necessariamente si applicano ad altri tipi di provider. In effetti qualunque provider deve derivare da ProviderBase che espone un metodo Initialize() tra i cui argomenti troviamo una NameValueCollection che contiene i parametri "sconosciuti" che verranno gestiti direttamente dall'implementazione che stiamo realizzando.

Se vi trovare perciò a dover creare un vostro sistema di provider, ma anche nel caso in cui vogliate scrivere un sistema pluggabile, in cui i moduli aggiuntivi possano godere di parametri di configurazione propri espressi all'interno dell'xml nel web.config dovrete gestire tali parametri in nel ConfigurationElement che li prevede. Ecco un esempio di come realizzare questo meccanismo:

public class ApplicationInfo : ConfigurationElement
{
    
private NameValueCollection attributes = new NameValueCollection();

    [ConfigurationProperty("name")]
    
public string Name
    {
        
get return (string)this["name"]; }
        
set this["name"] = value; }
    }

    
//
    // TODO: definire le altre proprietà di default
    //

    
public NameValueCollection Attributes
    {
        
get return attributes; }
    }

    
protected override bool OnDeserializeUnrecognizedAttribute(string name, string value)
    {
        attributes.Add(name, 
value);
        
return true;
    }
}

OnDeserializeUnrecognizedAttribute e il complementare OnDeserializeUnrecognizedElement sono una coppia di metodi virtuali che verranno chiamati dal runtime durante la fase di deserializzazione della configurazione e consentono di gestire eventuali attributi o elementi imprevisti. Nel caso concreto il metodo non fa altro che aggiungere i valori ad una NameValueCollection e ritornare "true" per ogni attributo sconosciuto, informando il runtime che non se ne deve ulteriormente occupare. Se invece volessimo gestire una serie specifica di attributi potremmo ritornare "false" qualora quello trovato non ci competa e in questo caso il runtime solleverà una eccezione che informa dell'errore di configurazione.

powered by IMHO 1.3


Ho scritto una articoletto su come ottenere dalla GridView un dato di cui è gelosa custode. Si tratta di VirtualItemCount che chi ha utilizzato la vecchia DataGrid conosce bene. L'articolo, che esplora mediante Reflector il meccanismo di paging della GridView è molto interessante anche per capire le limitazioni del controllo in alcuni casi.

link: ASP.NET 2.0: Ottenere il VirtualItemCount dalla GridView


Ormai sono abituato ad evitare a tutti i costi i popup nelle applicazioni web. Tuttavia mi rendo conto che c'è ancora qualcuno che non riesce a farne a meno, a torto o a ragione non mi interessa sapere. Durante la track web dei CommunityDays sono stato molto incuriosito da una domanda che è venuta dal pubblico e che recitava più o meno così: "è possibile effettuare il cross-page postback in un popup impostando il PostBackUrl?". La risposta naturale è no, ma quest'oggi, per puro diletto ho provato a capire se questo è proprio vero. In effetti se ragioniamo bene sul problema è evidente che esiste almeno un modo di effettuare il postback in una nuova finestra del browser, cioè impostando l'attributo "target" della form. Però impostare questo attributo a runtime mediante javascript è fattibile ma espone ad un fastidioso problema. Il mio primo tentativo è stato quello "semplice" di usare l'evento "OnClientClick" come segue:

btnPopup.OnClientClick = "document.forms[0].target = '_blank';";

Impostare l target in questo modo funziona perfettamente, ma lascia la form in uno stato inconsistente che fa si che tutti i controlli presenti effettuino il postback nella nuova finestra dopo che il pulsante è stato azionato per la prima volta. In effetti la cosa migliore sarebbe quella di ripristinare il target originale dopo il postback, ma in questo l'evento OnClientClick non ci può essere di aiuto perchè il runtime quando fa il merge degli script aggiunge il codice per il postback subito dopo quello immesso in questo attributo. La soluzione perciò è solamente quella di estendere il controllo Button per modificarne il rendering:

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.IO;
using System.Text.RegularExpressions;

namespace Elite.Web.UI.WebControls
{
    
/// <summary>
    /// 
Summary description for ButtonEx
    
/// </summary>
    
public class ButtonEx : Button
    {
        
private string postBackTarget;

        
public ButtonEx()
        { }

        
protected override void Render(HtmlTextWriter writer)
        {
            
if (!string.IsNullOrEmpty(this.PostBackUrl) && 
                !
string.IsNullOrEmpty(this.PostBackTarget))
            {
                
using (StringWriter tempWriter = new StringWriter())
                {
                    
using (HtmlTextWriter htmlWriter = new HtmlTextWriter(tempWriter))
                    {
                        
base.Render(htmlWriter);
                        
                        Regex rx = 
new Regex(
                            @"onclick=""javascript:(?<code>[^""]*)""", 
                            RegexOptions.IgnoreCase);

                        writer.Write(
                            rx.Replace(
                                tempWriter.ToString(), 
                                
new MatchEvaluator(Evaluator)));
                    }
                }
            }
            
else
                base
.Render(writer);
        }

        
private string Evaluator(Match match)
        {
            
return string.Format(
                @"onclick=""javascript:document.forms[0].target='{0}';{1};" + 
                 "setTimeout(&quot;document.forms[0].target=''&quot;, 100);""",
                
this.PostBackTarget, 
                match.Groups["code"].Value);
        }

        
public string PostBackTarget
        {
            
get return postBackTarget; }
            
set { postBackTarget = value; }
        }
    }
}

Il codice è in effetti un po' tirato, lo confesso, ma è davvero l'unico modo che sono riuscito a trovare, dopo aver analizzato la classe Button del framework, per iniettare il codice prima e dopo la generazione del postback. in sostanza nel metodo render viene fatto generare il codice del pulsante in una stringa, e tramite una regular expression si effettua la sostituzione all'interno dell'evento onclick. Questo codice viene poi immesso nuovamente nel flusso del rendering. In questo modo impostando la proprietò PostBackTarget del ButtonEx si otterrà il sospirato postback in popup. L'unica curiosità che rimane è il fatto che ho dovuto usare un timeout di 100 millisecondi per reimpostare il target. Infatti semplicemente impostando la proprietà subito dopo WebForm_DoPostBackWithOptions non si ottiene l'effetto desiderato.

powered by IMHO 1.3

 


Mi è capitato di avere la necessità di creare all'interno di una colonna di gridview del codice javascript il quale potesse in qualche modo ottenere alcuni dei dati in corso di visualizzazione nella gridview stessa. Ad esempio, in riferimento alle popup di tipo subModal, di cui vi ho parlato in precedenza mi è stato necessario comporre dinamicamente l'url da passare alla chiamata Javascript che ne causa la comparsa.

La gridview, a meno che non abbia preso un abbaglio, non dispone di una campo che permetta la formattazione di codice lato client, a meno che non si voglia utilizzare un TemplateField. Inutile anche tentae di introdurre il prefisso "javascript:" l'url formattato infatti il runtime di ASP.NET per questioni di sicurezza lo sopprime senza colpo ferire. Ho perciò deciso di indagare un po' sul funzionamento della gridview per capire se era possibile estendere l'HyperLinkField in modo da gestire anche la formattazione di un ClientLink al pari di quello che si può fare con il NavigateUrl. Ecco il codice che ne ho tirato fuori:

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Collections.Generic;
using System.ComponentModel;

namespace Elite.Web.DataSources
{
    
/// <summary>
    /// 
Classe che implementa AdvHyperLinkField
    
/// </summary>
    
public class AdvHyperLinkField : HyperLinkField
    {
        
/// <summary>
        /// 
ctor
        
/// </summary>
        
public AdvHyperLinkField()
        { }

        
/// <summary>
        /// 
Inizializza una cella della GridView
        
/// </summary>
        /// <param name="cell">
cella</param>
        /// <param name="cellType">
tipo di cella</param>
        /// <param name="rowState">
stato della riga</param>
        /// <param name="rowIndex">
indica della riga</param>
        
public override void InitializeCell(
            DataControlFieldCell cell,
            DataControlCellType cellType,
            DataControlRowState rowState,
            
int rowIndex)
        {
            
// dapprima chiama il metodo base
            
base.InitializeCell(cell, cellType, rowState, rowIndex);

            
// se si tratta di una DataCell
            
if (cellType == DataControlCellType.DataCell &&
                cell.Controls.Count > 0)
            {
                
// trova l'hyperlink
                
HyperLink link = cell.Controls[0] as HyperLink;

                
// e aggancia l'evento DataBinding
                
if (link != null)
                    link.DataBinding += 
new EventHandler(link_DataBinding);
            }
        }

        
/// <summary>
        /// 
Gesisce l'evento che viene sollevato durante il DataBinding
        
/// </summary>
        /// <param name="sender">
il link che ha generato l'evento</param>
        /// <param name="e">
i dati dell'evento</param>
        
void link_DataBinding(object sender, EventArgs e)
        {
            
// trova il link che ha generato l'evento
            
HyperLink link = sender as HyperLink;
            
// preleva il controllo container
            
Control container = link.NamingContainer;

            
if (container != null)
            {
                
bool found;

                
// trova il DataItem corrente nel Container
                
object dataItem = DataBinder.GetDataItem(container, out found);

                
if (found)
                {
                    
// se lo ha trovato crea una list con le traduzioni
                    
List<string> paramList = new List<string>();

                    
// traduce ogni campo richiesto nel dato corrispondente
                    
foreach (string name in this.DataNavigateUrlFields)
                        paramList.Add(
                            DataBinder.GetPropertyValue(
                                dataItem, 
                                name) 
as string);

                    
// formatta l'url clent
                    
link.Attributes["onclick"] = 
                        
string.Format(
                            
this.DataClientNavigateUrlFormatString, 
                            paramList.ToArray());

                    
// assegna un url fittizio se non è giù stato 
                    // assegnato (altrimenti l'hypelink non appare)
                    
if (!string.IsNullOrEmpty(link.NavigateUrl))
                        link.NavigateUrl = "~/";
                }
            }
        }

        
/// <summary>
        /// 
proprietà contente i campi da utilizzare nella formattazione
        
/// </summary>
        
public string[] DataClientNavigateUrlFields
        {
            
get
            
{
                
if (ViewState["DataNavigateUrlFields"] == null)
                    ViewState["DataNavigateUrlFields"] = 
new string[0];

                
return (string[])ViewState["DataNavigateUrlFields"];
            }
            
set { ViewState["DataNavigateUrlFields"] = value; }
        }

        
// stringa da formattare
        
public string DataClientNavigateUrlFormatString
        {
            
get
            
{
                
if (ViewState["DataClientNavigateUrlFormatString"] == null)
                    ViewState["DataClientNavigateUrlFormatString"] = 
string.Empty;

                
return (string)ViewState["DataClientNavigateUrlFormatString"];
            }
            
set { ViewState["DataClientNavigateUrlFormatString"] = value; }
        }
    }
}

 

Questo nuovo tipo di campo, derivato direttamente da HyperLinkField, aggiunge due proprietà che consentono di decidere come formattare il codice eseguito in corrispondenza dell'evento javascript "onClick". DataClientNavigateUrlFields e DataClientNavigateUrlFormatString si comportano esattamente come le proprietà esposte da HyperLinkField e dedicate all'url. Vediamo un attimo di sottolinearne il funzionamento: La classe fa innanzitutto l'override del metodo InitializeCell. Questo metodo viene chiamato in corrispondenza di ogni cella della gridview e consente di aggiungere elementi al contenuto. Nel nostro caso dapprima viene richiamato il metodo base che si occupa di creare l'HyperLink nella cella, in seguito nel viene cercato tale link e ad esso si aggancia l'evento DataBinding solo nel caso di celle di tipo DataCell (escludendo quindi header e footer). In questo modo, ad ogni evento di bind dell'HyperLink di ogni cella verrà chiamato il metodo link_DataBinding() che estrae il DataItem dal contesto della GridView ed effettua il binding delle due nuove proprietà.

Nello stesso metodo c'è anche un piccolo stratagemma da evidenziare. Qualora il campo NavigareUrl sia vuoto, si inserisce un url fittizio (~/) sempre valido, in modo da provocare la comparsa dell'hyperlink che in caso contrario non verrebbe mostrato.

powered by IMHO 1.3


La discussione in merito all'uso di popup nelle applicazioni web infiamma sempre gli animi. Personalmente sono dalla parte di chi cerca di evitarli a tutti i costi perchè il loro utilizzo va contro quello che è il normale funzionamento di una pagina web. Riconosco però che l'uso di popup, specialmente in quelle che io definisco "applicazioni web" in contrapposizione ai "siti web", è spesso necessario per abbreviare la navigazione e migliorare l'interazione dell'utente.

In questi giorni ho scovato nella rete un libraria javascript, denominata subModal, che permette di risolvere questo problema con una eleganza impareggiabile. Si tratta di un tool che è in grado di realizzare facilmente popup implementate come dei semplici DIV/IFRAME, all'interno della stessa pagina, eliminando così la necessità di aprire finestre aggiuntive del browser. In questo modo si risolve anche il problema del blocco popup.

La libreria è completamente free, ed è compatibile con Internet Explorer, Firefox, Safari e Opera. Vi invito a vedere la demo che ne chiarisce molto le potenzialità.

Link: http://www.subimage.com/sublog/subModal

powered by IMHO 1.3


Chi come me sviluppa in ambiente web, è abituato a fare uso, talvolta anche abuso della Cache che il runtime http integra nel framework ASP.NET. Non tutti sanno però che questa cache, può essere abbastanza facilmente essere usata anche in applicazioni Windows Forms. In soldoni è sufficiente creare un runtime http fittizio che supporti adeguatamente la cache e il gioco è fatto.

In realtà si tratta di un trucco abbastanza vecchio e conosciuto, ma quest'oggi ho finalmente deciso di risolvere con problema in modo definitivo. Il fatto è che spesso è utile incapsulare il caching all'interno di classi che poi possono trovare utilità indifferentemente  in applicazioni WebForms o WindowsForms. Perciò sarebbe utile avere una cache che si adegui al contesto in cui viene istanziata in modo trasparente. Vi propongo perciò in allegato a questo post il codice che ho realizzato, accompagnato da uno schema tratto dal class diagram di Visual Studio 2005 per esporvi la soluzione.

Le tre classi che potete vedere a fianco pur nella loro semplicità fanno uso di due Design Pattern. Il CacheManager si comporta da un lato come una Abstract Factory, consentendo di istanziare con il metodo statico Create() un'istanza concreta dedicata all'ambiente in cui il processo è in esecuzione. Il test è molto semplice: se è presente un HttpContext verrà istanziato l'HttpCacheManager, in caso contrario la scelta ricaderà sul WinFormsCacheManager. La classe CacheManager inoltre è un Singleton. Essa espone una proprietà Current la quale rende disponibile il CacheManager corretto istanziato per mezzo di Create(). Questa organizzazione consente di avere accesso alla cache adatta al contesto per mezzo di una sola riga di codice:

object cached = CacheManager.Current["cachedObject"];

La classe concreta che implementa il CacheManager espone una serie di proprietà e metodi molto comodi. Add() che consente di aggiungere un item in cache, e IsCached che data una chiave verifica se essa è già presenti in cache. Quest'ultimo metodo permete di scrivere qualcosa del genere:

public List<Product> GetProduct(int sku)
{
    
if (!CacheManager.Current.IsCached(sku))
        CacheManager.Current.Add(
            sku, 
            GetProductFromDB(sku), 
            TimeSpan.FromSeconds(20));

    
return CacheManager.Current[sku] as Product;
}

Download: Elite.Utilities.Caching.zip (42 KB)

powered by IMHO 1.3


Mi sono finalmente deciso ad investire qualche minuto del mio tempo per farmi una macro che mi aiuti a risparmiare le decine di secondi che di solito impiego per fare l'attach al processo di aspnet_wp.exe, per debuggare un website in IIS su XP. Di solito preferisco questo metodo perchè così posso tenere aperto un browser sulla pagina che sto realizzando e non debbo eseguire uno stesso path all'interno del sito per arrivarci ogni volta con il webserver integrato di Visual Studio 2005.

Ecco la macro scritta in puro VB.NET (arrggggggg!!!)

Imports System
Imports EnvDTE
Imports EnvDTE80
Imports System.Diagnostics
Imports System.IO

Public Module AspNetMacros

    
Sub AttachASPNET()

        
Dim aspnet As EnvDTE.Process = 
            FindASPWP(_ApplicationObjects.DTE)

        
If Not aspnet Is Nothing Then

            
aspnet.Attach()

            System.Threading.Thread.Sleep(2000)

        
Else

            MsgBox
("Cannot find aspnet_wp.exe")

        
End If

    End Sub


    Public Function 
FindASPWP(ByRef dte As EnvDTE.DTE) As EnvDTE.Process

        
For Each proc As EnvDTE.Process In dte.Debugger.LocalProcesses
        
            
If (Path.GetFileName(proc.Name) = "aspnet_wp.exe") Then
                Return 
proc
            
End If
        
        Next

        Return Nothing

    End Function

End Module

Naturalmente potete anche customizzarla per fare l'attache al processo che desiderate. Spero che faccia risparmiare qualche ora di lavoro anche a voi.

powered by IMHO 1.3