Consumiendo APIs desde ASP.NET MVC

Hoy me preguntó alguien que está aprendiendo ASP.NET MVC cuál era la mejor forma de consumir una API usando este framework; y no seré yo quien le responda cuál es la mejor forma, pero al menos era una pregunta que da para post, y no está mal que me critiquéis mi solución.

La API la vamos a consumir desde el servidor, ya que consumirla desde JavaScript parece cumbersome; aunque puede tener sus ventajas si tenemos restricciones por IP, aunque sea jugar sucio, al mismo tiempo podríamos estar exponiendo datos que no nos interesan en una aplicación cliente, o podríamos tener el caso en el que el cliente no tenga JS activado, caso tan raro como cierto.

Al lío: lo primero que vamos a hacer es instalar Unity en el proyecto Web de la solución si usamos MVC 5, o cantar algo si ya usamos ASP.NET vNext, ya que usa inyección de dependencias de por sí, por lo que no tendremos que preocuparnos por el contenedor.

Después de un paso obvio, vamos a definir la interfaz que será la fundación sobre la cual vamos a desarrollar nuestra propia abstracción sobre la API externa, pokeapi.co, en este ejemplo. Lo primero que haremos será definir una abstracción sobre HttpClient.

    public interface IHttpCommunicator
    {
        string BaseAddress { get; set; }

        Task<string> SendGetRequestAsync(string uri);
        Task<string> SendPostRequestAsync<T>(string uri, T data);
    }

Una vez tenemos una abstracción sobre el servicio de comunicación, tendremos que definir una abstracción sobre la API que usaremos, con los métodos que usaremos, donde inyectaremos el servicio HTTP y haremos uso de él:

    public interface IPokeApiClient
    {
        Task<Pokemon> GetPokemon(int id);

        Task<Ability> GetAbility(int id);

        Task<Models.Type> GetType(int id);
    }

Y, finalmente, inyectaremos la abstracción de la API externa en nuestros controladores, u otros servicios donde la vayamos a usar:

        [HttpPost]
        public async Task<ActionResult> Pokemon(int id)
        {
            var pokemon = await _pokeApiClient.GetPokemon(id);
            var vm = new PokemonViewModel
            {
                Pokemon = pokemon
            };
            return View(vm);
        }

Aunque no será la mejor implementación, proporciona una abstracción útil y sencilla sobre APIs de terceros, a pesar de provocar una dependencia temporal sobre definir la BaseAddress desde un punto externo, pero nos permite reutilizar el servicio de HTTP en cualquier parte.

La solución completa se puede encontrar en GitHub.


UpperCounter Kata con LINQ, primera parte

Hace unos días Roberto Luis publicaba una kata con la que habían estado trabajando en Software Craftmanship Madrid, y me animaba a resolverla. Y no iba yo a negarle tal sugerencia a Roberto, así que aquí está mi intento con LINQ; el vídeo lo dejamos para otra ocasión.

Toda la solución se basa en una sobrecarga del método de extensión Select, que a poca gente he visto usar y que nos permite proyectar, además del elemento sobre el que se está trabajando, el índice del mismo en una lista…¿qué más se puede pedir?

Pues eso, unido a un selector que nos provea sólo los elementos que son mayúscula y ¡magia! Y tanto que magia, porque a ver quién debuggea esto luego si se complica el asunto en un caso de la vida real donde se sustituye un foreach por LINQ. Quedará mucho más h4x0r en código, pero a veces no es necesario, querido R#. En todo caso quería intentarlo, y queda así:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace UppercaseSearcher
{
    class UppercaseSearcher
    {
       public static List<int> Search(string input)
        {
            return input.Select((ch, index) => new { index, ch }).Where(x => char.IsUpper(x.ch)).Select(t => t.index).ToList();
        }
    }
}

Paso a paso se ve más sencillo: lo primero que asumimos es tratar la string como un array de chars, de ahí, el primer Select proyecta cada uno de los elementos a un tipo anónimo que no es más que una tupla int,char; seleccionamos sólo los elementos que satisfagan la condición del kata, y aplanamos dicho objeto anónimo para que sólo nos devuelva los índices que los que pasaron la condición.

Sobre los tests, aunque fueron iguales a los de Roberto, y una segunda parte con restricciones hablaremos más adelante, si la solución que se me ocurre no es tan parecida a la de él :).


Rewriting ¿better? WCF RIA Services callbacks while reusing code

There’s a legacy project I’ve been working on that has recently had it’s environment migrated to a different architecture, meaning, problems have arisen, but manageable problems. Until now.

The affected part is a Silverlight app that connects to a WCF RIA Service on a server, and then this server connects to a CRM 4 service and does some CRUD operations on CRM entities. The Silverlight scenario is fairly common, but rather exhausting: ExpedientViewModel is the VM to a child window of InterviewViewModel, which inherits from BaseInterviewViewModel, which inherits from BaseTaskViewModel that, finally, inherits from BaseViewModel. ExpedientViewModel is in charge of creating/editing a set of entities, that is then handed over to InterViewViewModel (remember, parent-child window) through MvvmLight messaging.

Leaving aside the discussion about the architecture, the problem was one of the entities was failing on an insert and we couldn’t debug why, because every call to Context.SubmitChanges() was done with the parameterless overload, meaning you had to do something like this:

var operation = Context.SubmitChanges();
operation.Completed += SubmitChangesCompleted;
//...
private void SubmitChangesCompleted(object sender, EventArgs args)
{
    ///do something else once the data is "inserted". There's no way to get that operation variable that actually holds information about the operation in here. And I'm doing a global variable for that.
}

The problem was sound: the Complete operation does not say anything about it being completed with or without errors, nor there’s any way to actually get the OperationBase.SubmitOperation object on the callback to determine if anything has gone wrong. The solution goes without mention: use the SubmitChanges overload that takes 2 parameters, an Action callback and an object userState. But then again, I didn’t wanted to rewrite the whole application again, including the callbacks within lambdas.

Having in mind I’m no architecture whatsoever, the solution I came up with was having a GeneralSubmitChangesCompleted function in the BaseViewModel taking a SubmitOperation and an Action to be passed as a callback to every SubmitChanges and, inside this function, call whatever function is next. Like so:

protected void GeneralSubmitChangesCompleted(SubmitOperation operation, Action<object, EventArgs> next)
{
    if(!operation.HasError)
    {
        next(new object(), new EventArgs());
    }
    else
    {
        var frame = new StackFrame(1);
        InsertLog(this.GetType().Name, frame.GetMethod().Name, new Exception(operationCompleted.Error.Message, operationCompleted.Error.InnerException));
    }
}

So far it works and I’m a bit more certain that the operation actually completes successfully and at least I’m able to discard that part as the cause of the problem. But I don’t know if it’s actually a good practice to follow.

¿Any expert?


How to avoid content being cached when using a CrmDataContext

Lately I’ve been working on a project that uses CRM and has a Silverlight app connected to it via a RIA Service. This RIA Service itself uses Microsoft.Xrm.Client.Data.Services.CrmDataContext to access the CRM’s data and it’s wrapped in a DomainService in order to make things easier.

Thing is we’re migrating a part of the app into an ASP.NET app, while reusing the RIA Service’s code by cloning the DomainService. So far so good, because besides making the proper changes to the app’s code, which may seem non-trivial some times, the DomainService works.

The problem is we have and use a lot of child windows wrapped into iframes that connect themselves to CRM. So, for example, if an iframed child window updates a contact details, the parent window will not be aware of the changes…unless you make an AJAX request when the child window is closed and refresh the client’s data; or that’s what you may think, but it doesn’t work, because the Domain Services objects are different in each window and the parent’s Context has content being cached. In Silverlight there’s no problem, because both child and parent windows share the same Context, that’s not possible in ASP.NET without being a mess.

The solution

Remove the cache, right? Well, no, one does not simply remove the cache, nor kill the Batman; it’s undocumented, but there’s a way. 2, actually:

Clearing the cache for a single entity type

 public void ClearCache(string entityName)
{
    var format = "adxdependency:crm:entity:{0}";
    var dep = string.Format(format, entityName).ToLower();
    var cache = CacheManager.GetBaseCache();
    cache.Remove(dep);
}

Or:

CacheManager.GetBaseCache().Remove(string.Format("adxdependency:crm:entity:{0}", entityName));

Clearing the cache for every single entity

BaseCache baseCache = CacheManager.GetBaseCache();
foreach (string x in from x in baseCache
        where x.Key.Contains("adxdependency:crm:entity:")
        select x.Key)
    baseCache.Remove(x);

Or, with an extension method:

public static void Clear(this Cache x)
{
    List<string> cacheKeys = new List<string>();
    IDictionaryEnumerator cacheEnum = x.GetEnumerator();
    while (cacheEnum.MoveNext())
    {
        cacheKeys.Add(cacheEnum.Key.ToString());
    }
    foreach (string cacheKey in cacheKeys)
    {
        x.Remove(cacheKey);
    }
}

Generar datos de ejemplo a partir de clases y NBuilder en ASP.NET MVC 4

A estas alturas creo que no es muy necesario introducir lo bueno de los datos de ejemplo en las aplicaciones, sobre todo al momento de probar el diseño de las mismas, o hacer todo tipo de pruebas y perrerías que nos podría meter algún usuario en las vistas, aunque normalmente estas deberían estar controladas aunque sea con un if(), idealmente con anotaciones sobre el modelo. Con NBuilder podemos tener datos de ejemplo para nuestras vistas de ASP.NET MVC 4 de una manera ultra-sencilla, generándolos directamente en el Controller. Voy a suponer que ya habréis creado un nuevo proyecto de MVC 4 e instalado NBuilder desde Nuget. Vaya por delante que esta entrada no pretende ser más que una pequeña introducción a una herramienta que me está resultando útil estos días.

Primero que nada, cabe subrayar cómo NBuilder nos genera los datos de prueba, que dependen del tipo de datos que pretendas generar. Por ejemplo, para las strings será algo como string.Format({0}{1},propertyName, i) dentro de un bucle. Es decir, si tenemos un campo “Name” que es una string, los datos que nos generará serán “Name1, Name2,…,NameN”. Para los números es tan sencillo como un i++ desde 1 a N (1,2,3…N). Para las fechas, parte de la actual y le va sumando un día.

Bien, supongamos que tenemos la siguiente entidad (escribiré el código directamente en WordPress, sabrán perdonarme alguna identación):

public class Event
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Venue { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
}

Entidad modelo sencillísima donde se podrían hacer mil cosas más, así como optimizaciones agrupando en un objeto varias propiedades…pero eso será ejercicio del lector, vamos al grano de lo que queremos con NBuilder. Ahora, en el controlador, tendremos que importar dos namespaces, el de nuestro modelo y Fizzware.NBuilder. Una vez hecho esto podremos escribir un código parecido a este en nuestro controlador:

public ActionResult Index()
{
    var events = Builder.CreateListOfSize(10).Build();
    return View(events);
}

Asumiendo que la vista la habréis creado con clic derecho en la acción -> crear vista tipada, el trabajo está hecho. Pero dejarlo así es un poco soso, teniendo en cuenta las capacidades de creación de contenido casi como queramos que nos provee NBuilder. Por ejemplo, supongamos que queremos hacer que todos los datos sean iguales. No sé bien por qué, pero supongamoslo:

public ActionResult Index()
{
    var events = Builder.CreateListOfSize(10).All().With(x=>x.Id = 1).And(x=> x.City = "Valencia").And(x=> x.Country = "Spain").And(x=> x.Venue = "L'umbracle").Build();
    return View(events);
}

O mejor aún, algo más elaborado, donde tendremos los primeros N elementos de una forma, los siguientes N de otra, y así sucesivamente:

public ActionResult Index()
{
    var events = Builder.CreateListOfSize(10).All().TheFirst(2).With( x=> x.Id = 1).TheNext(3).With( x=> x.City = "Madrid").TheNext(3).With(x=>x.Country = "USA").TheLast(2).With(x=>x.Venue = "Microsoft").Build();
    return View(events);
}

Por supuesto podéis usar NBuilder en otros proyectos, así como con tipos de datos complejos, pero…ya tenéis la idea 🙂


Cómo hacer que PointerPressed y PointerReleased funcionen en un botón de Windows 8 con C#

Hoy, haciendo algunas pruebas para el trabajo, tuve la necesidad de detectar sólo cuando el usuario está -activamente- presionando un botón. Es decir, no el evento click/tap ni un hold, sólo cuando tiene el click/dedo encima del botón mientras lo mantiene presionado. Esto se corresponde con los eventos PointerPressed y PointerReleased de los elementos visuales.

El problema, en Windows 8, está en que estos eventos no se disparan correctamente en un botón, directamente no se disparan. Algo disparatado, realmente, teniendo en cuenta que es un elemento visual que está hecho específicamente para ser presionado. Sea como fuere, puede haber dos soluciones:

  1. Si necesitas varios botones pulsados a la vez entonces, la solución no es usar un botón, sino un Grid, como bien me sugirió Adrián Fernández.
  2. Si sólo necesitas un botón a la vez, la solución pasa por hacerlo de una manera más rústica que sólo suscribiéndose al PointerPressed y PointerReleased, con el siguiente código:
BotonQueNecesites.AddHandler(PointerPressedEvent, new PointerEventHandler(BotonQueNecesites_PointerPressed), true);

Recuerda que puedes usar Ctrl+. sobre BotonQueNecesites_PointerPressed para poder crear el método con los argumentos correctos, sin necesidad de sabértelos de memoria. Así pues, el caso contrario par el PointerReleased resulta análogo:

BotonQueNecesites.AddHandler(PointerReleasedEvent, new PointerEventHandler(BotonQueNecesites_PointerReleased), true);

A partir de esto, sólo queda definir los handlers:

private void BotonQueNecesites_PointerReleased(object sender, PointerRoutedEventArgs e)
{
     output.Text = "Released";
}

private void BotonQueNecesites_PointerPressed(object sender, PointerRoutedEventArgs e)
{
     output.Text = "Pressed";
}

Habilitar Single Sign-On con Mobile Services y el Live SDK en Windows Phone 8 y Windows 8 con C#

Vamos al grano, lo que queremos: autenticar automágicamente a un usuario cuando se abra la aplicación, que sólo tenga que meter su usuario y contraseña una sola vez, la primera vez que abre la app.

Como soluciones posibles, Quique y yo, fuimos a por la forma “sencilla” en WinJS: serializar y guardar el usuario en local, luego checkear el usuario antes de intentar autenticarlo otra vez, pero hay una mejor forma de hacerlo: habilitar el Single Sign-On del Live SDK, autenticarte con él en la aplicación de Live que debes tener creada igualmente si quieres el provider de la Microsoft Account en Mobile Services y luego usar la sobrecarga del método LoginAsync que toma como argumento una string con el token que te devuelve la autenticación del Live. Vamos allá:

En Windows Phone 8

En un método Authenticate que queráis. Como nota, en Windows Phone 8, los métodos de autenticación deben ir en el Loaded, más que en el OnNavigatedTo para no hacer guarradas, ya que en WP8 se navega a otra página para hacer el logueo y pedir permisos, no así en W8, por lo que tendríais 2 OnNavigatedTo, causando una excepción si no implementáis un centinela (caí en esto gracias a Gorka en el mail de soporte):


while (liveSession == null)
 {

var authClient = new LiveAuthClient("<<TU ID DE APP>>");

string [] reqs = new[] { "wl.signin", "wl.basic", "wl.offline_access" };
 var init = await authClient.InitializeAsync(reqs);

var session = authClient.Session;

if (init.Status != LiveConnectSessionStatus.Connected)
 {
 MessageBox.Show("This app needs an authenticated user to work, please check mygym.azurewebsites.net  for info & privacy", "Please Log in", MessageBoxButton.OK); //cambia por tu sitio
 var logStatus = await authClient.LoginAsync(reqs);

if (logStatus.Status == LiveConnectSessionStatus.Connected)
 {
//primer login
 session = logStatus.Session;
 liveSession = session;
 LiveConnectClient client = new LiveConnectClient(liveSession);
 LiveOperationResult meResult = await client.GetAsync("me");
 string title = string.Format("Welcome {0}!", meResult.Result["first_name"]);
 MessageBox.Show("Welcome to My Gym for the first time, please check mygym.azurewebsites.net for more info or how-to.", title, MessageBoxButton.OK); //reemplazad con vuestros datos
 user = await mobileServiceClient.LoginAsync(session.AuthenticationToken); //esta es la autenticación hacia Mobile Services
 }
 }

if (init.Status == LiveConnectSessionStatus.Connected)
 {
 //el resto de logins
 liveSession = session;
 user = await mobileServiceClient.LoginAsync(session.AuthenticationToken); //autenticación contra Mobile Services
 }

}

Con esto ya lo tenéis acabado, sólo os pedirá user/pass la primera vez que se ejecute la app después de cada instalación.

En Windows 8

Aquí es más o menos lo mismo, aunque el Authenticate puede ir en un OnNavigatedTo, ya que internamente usa WebAuthBroker, que no navega a otra página, sino que saca un pop-up. Lo que cambia es la forma de inicializar el cliente de Live, que no se inicializa con el ID de la app, sino con la URL de callback, además de haber simplificado el código, quitando un par de llamadas al Live para obtener el nombre, por ejemplo:

var authClient = new LiveAuthClient("<<URL DE TU SERVICIO>>");

            reqs = new[] { "wl.signin", "wl.basic", "wl.offline_access" };
            var init = await authClient.InitializeAsync(reqs);

            var session = authClient.Session;

            if (init.Status != LiveConnectSessionStatus.Connected)
            {
                //hay que autenticarse
                var logStatus = await authClient.LoginAsync(reqs);

                if (logStatus.Status == LiveConnectSessionStatus.Connected)
                {
                    //primera autenticación
                    session = logStatus.Session;
                    user = await mobileServiceClient.LoginAsync(session.AuthenticationToken);
                }
            }

            if (init.Status == LiveConnectSessionStatus.Connected){
            //resto de logins
            user = await mobileServiceClient.LoginAsync(session.AuthenticationToken);

            }

¡Todo hecho!. ¿Dudas? Tenéis los comentarios 🙂


Cómo depurar remotamente en Windows RT

escritorio

Si habéis sido buenos estas navidades, a muchos -ojalá- os habrá quedado el escritorio con una configuración parecida a la del mio. En mi caso fue una Asus Vivo Tab RT por cortesía del Desafío Windows 8 de Microsoft. Al ser arquitectura ARM, Visual Studio 2012 no entra en ella, pero aún así nos servirá como un dispositivo de prueba de nuestros desarrollos, además, un entorno diferente a nuestro PC, haciendo más variado -y divertido– el testing. En fin, que si has entrado aquí es porque sabes qué es lo que esperabas encontrar, así que vamos al lío:

Continue Reading


Cómo solucionar “Hacked by Gabby” en WordPress

Ninguna plataforma es 100% fiable ni segura, mucho menos cuando tienes un hosting compartido con cPanel, como es mi caso. Por ello, esta noche tuve una intromisión en este blog; afortunadamente todo estaba con copia de seguridad pertinente.

El problema fue el “Hacked by Gabby”, parecido al “hacked by hacker”, pero algo más enrevesado y complicado de solucionar, ya que Google no arroja ningún resultado de momento, además de no dejar acceder al wp-admin. En principio reinstalé todo WordPress, pero el error continuaba -aunque ya tenía acceso al wp-admin-, aunque por un momento lograba ver mi web anterior, antes de ser reemplazada por la molesta frase. Buscando en todos los archivos de PHP buscaba la frase, incluso cortada; también en los ficheros de JS del tema, pensando que era una redirección o sobrescritura en el onready. Pero era algo más fácil, una línea de código insertada desde un widget de texto en la barra lateral que reemplazaba el innerHtml del documentElement.

document.documentElement.innerHTML = unescape('%48%61%63%6b%65%64%20%42%79%20%47%61%62%62%79');

Nótese la malicia poniendo el texto.

Conclusión

  1. Para tener acceso al wp-admin reinstala WordPress por FTP. (Nota: no sé si habrá una forma más fácil de hacerlo).
  2. Ve a Apariencia->Widgets.
  3. Busca un Widget de texto con el trozo de código arriba mencionado.
  4. Elimina dicho widget.
  5. Ya tienes tu blog sin el “Hacked By Gabby” :).

Cómo enviar comentarios a WordPress desde Windows Phone [C#]

Por si alguna vez necesitáis enviar comentarios a un blog de WordPress desde vuestra aplicación de Windows Phone 7 (en realidad se puede adaptar el código a otras plataformas con dos pequeños tweaks), lo que necesitáis hacer es lo siguiente:

##El problema, identificando requerimientos##

Necesitamos enviar un comentario a un blog de WordPress, se nos puede ocurrir que tenemos que enviar una petición con unos datos al blog. ¿A qué dirección? ¿Qué datos? ¿En qué forma?

WordPress tiene una página para eso, es http://nombreblog.com/wp-comments-post.php. En este caso, por ejemplo, es http://danielrozo.es/wp-comments-post.php. Ya tenemos resuelta la primera duda.

Ahora necesitamos saber qué datos debemos enviar, para ello sólo basta con mirar el formulario que aparece debajo de cada entrada de WordPress, o bien, mirar el código fuente de wp-comments-post.php y ver las peticiones POST que acepta, que son:

  • comment_post_ID.
  • author.
  • email.
  • comment.
  • url.

Con esto hemos respondido las preguntas que nos quedaban: debemos enviar todos estos datos en una petición de tipo POST a /wp-comments-post.php.

El diseño de una página que haga esto es muy sencillo, os lo dejo a vosotros (son textboxes y fuera).

La única duda que queda por contestar es ¿Cómo identifica wp-comments-post.php el post al que quiero enviar el comentario? Pues con el comment_post_ID que le enviamos. Como consecuencia de ello, necesitamos el ID del post, en los WordPress que no tengan vanity URLs es el número que sale después de ?p= en la dirección. Para los que sí la tengan, es necesario cogerlo del RSS. Por ejemplo, si vemos el RSS de esta página vemos que dentro de cada <item> hay un <guid>, este es el elemento que necesitamos tratar para coger el ID del post. Vamos a ver cómo hacerlo usando LINQ, que es una maravilla. Supondré que tenéis una clase Post con los atributos creados, getters, setters y esas cositas que hacen falta. Este código debéis encapsularlo en un método al que le pasáis la string del XML del RSS que descargáis.


StringReader stringReader = new StringReader(feedXML);
 XmlReader xmlReader = XmlReader.Create(stringReader);
 XDocument loadedPosts = XDocument.Load(xmlReader);
 XNamespace dc = "http://purl.org/dc/elements/1.1/";
 XNamespace content = XNamespace.Get("http://purl.org/rss/1.0/modules/content/");
 var data = from query in loadedPosts.Descendants("item")
 select new Post
  {
   NombreBlog=(string)query.Parent.Element("title"),
   Titulo = (string)query.Element("title"),
   Autor = (string)query.Element(dc + "creator"),
   Contenido = (string)query.Element(content + "encoded"),
   Fecha = (string)query.Element("pubDate"),
   Link = (string)query.Element("link"),
   ID=getId((string)query.Element("guid")),
   Imagen = getImage((string)query.Element(content + "encoded"))
  };
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
  feedListBox.ItemsSource = data;
});

Dejando de lado los detalles del asunto, sólo os hace falta el método getId, que es el siguiente:

</pre>
private string getId(string s)
 {
string [] sa=s.Split('=');
return sa[1];
 }

Separa la string que le pasas, que será de tipo http://nombreblog.com/?p=xxxx, y devuelve solo el “xxxx” que es el ID.

Ahora sólo nos queda montar la petición POST que será enviada, que será algo parecido a esto:


WebClient webClient = new WebClient();
 webClient.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded";
 var uri = new Uri("http://" + url + "/wp-comments-post.php?", UriKind.Absolute);
 StringBuilder postData = new StringBuilder();
 postData.AppendFormat("{0}={1}", "comment_post_ID", HttpUtility.UrlEncode(_sItem.ID));
 postData.AppendFormat("&{0}={1}", "author", HttpUtility.UrlEncode(nombreTb.Text));
 postData.AppendFormat("&{0}={1}", "email", HttpUtility.UrlEncode(correoTb.Text));
 postData.AppendFormat("&{0}={1}", "comment", HttpUtility.UrlEncode(comentarioTb.Text));
 postData.AppendFormat("&{0}={1}", "url", HttpUtility.UrlEncode(urlTb.Text));
 MessageBox.Show("" + postData);

webClient.Headers[HttpRequestHeader.ContentLength] = postData.Length.ToString();
 webClient.UploadStringCompleted += new UploadStringCompletedEventHandler(webClient_UploadStringCompleted);
 webClient.UploadProgressChanged += webClient_UploadProgressChanged;
 webClient.UploadStringAsync(uri, "POST", postData.ToString());

Obviamente necesitáis tener los textboxes montados en vuestro XAML e implementar los métodos que se llaman por UploadStringCompleted (que será mostrar un mensaje de “Comentario posteado!” o algo y el UploadProgressChanged, que vendrá a actualizar una barra de progreso que tengáis con el ProgressPercentage del UploadProgressChangedEventArgs que le paséis al método.

¡Esto es todo, saludos!


Páginas:12