Web Uygulamanız OWIN ile Masaüstünde

    Hatırlarsanız OWIN ile ilgili ilk makalemde sizlere artık uygulama sunucusu ve işletim sistemi bağımsız olarak web uygulamalarınızı sunabileceğinizden bahsetmiştim. Şimdiye kadar geldiğimiz noktaya bakacak olursak öncelikle Katana üzerinden, ardından IIS’te ve son olarak da kendi geliştirdiğimiz bir konsol uygulamasında web uygulamalarımızı sunabilmiştik. Bu makalemde konuya farklı bir açıdan yaklaşmak istedim; OWIN’i sadece uygulama sunucusu bağımsızlığı olarak algılamaktan öte, yazılımlarımızda oluşturabileceği etkileri örneklemek istiyorum.

   Hatırlarsanız bir kaç makale önce size Katana projesi yardımıyla kendi uygulamanız üzerinde nasıl OWIN uyumlu bir sunucu oluşturabileceğinizi ve üzerinde web uygulamanızı nasıl sunabileceğiniz paylaşmıştım. Bu makalemde de temelde benzer bir konuyu ele alacağım; ama tek bir farkla… Bu defa Windows Forms uygulaması üzerinden…

   “Peki neden Windows Form?” diye düşündüğünüzü duyar gibiyim. “Yani, konsol uygulamasından farklı ne olabilir ki?”… Temel olarak yine Katana yardımıyla OWIN uyumlu bir web sunucu oluşturacağız. Bu defa üzerine, yeni öğrendiğimiz statik dosya ve ASP.Net Web API’nin sunulması için gerekli katmanları ekleyeceğiz. Bu sayede hem sabit html, css ve imaj gibi dosyalarımızı zahmetsizce yayınlayabilecek, hem de Web API sayesinde dinamik içerik için ihtiyacımız olan veri servislerini açabileceğiz. Bu kadar işlem ardında artık gidip bir browserdan web uygulamanızı açın demenin bir anlamı yok! Madem ki bir Windows Forms uygulaması, bir masaüstü uygulaması geliştiriyoruz; o zaman ana ekrana kocaman bir Webbrowser kontrolü ekleyelim!

   Uygulamamıza neden mi bir web browser gömüyoruz?! Çok basit; bu sayede kullanıcının uygulamamızdan başka bir yere ayrılmasına gerek kalmıyor.. Şaka bir yana, asıl neden kullanıcıya bir web browser açtırmak demek tüm büyünün kaybolması demek olacak da o yüzden. Web için geliştirdiğimiz, bir browser üzerinden hizmet veren uygulamamız aslında çok fazla değişmeden bir masaüstü uygulaması haline, internet bağlantısı olmadan da çalışabilir hale gelecekte bu yüzden… Arka plandaki bu sihir bırakalım bize kalsın, bırakın son kullanıcı aynı arayüze sahip; fakat masaüstü için geliştirmiş bir uygulama kullandığını düşünsün…

  İşe Visual Studio’yu açıp bir Windows Forms projesi oluşturmakla başlayalım. Ardından detaylarını daha önceki makalelerimde bulabileceğiniz gibi OWIN hosting, ASP.Net Web API ve statik dosya sunumu için gerekli NuGet paketlerini kuralım. Artık hazırız…

   Program.cs dosyasının içeriğini aşağıdaki şekilde değiştirerek uygulamamız açılır açılmaz sunucumuzu da başlatalım;

using System;
using System.Windows.Forms;
using Microsoft.Owin.Hosting;

namespace Enterprisecoding.OwinUI {
    static class Program {
        public const string SUNUCU_ADRESI = "http://localhost:8080";

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main() {
            using (WebApp.Start<Startup>(SUNUCU_ADRESI)) {
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                Application.Run(new MainForm());
            }
        }
    }
}

   Her zamanki gibi static dosyalar ve ASP.Net Web API için gerekli ayarlamaları Startup sınıfı içinde yapıyoruz;

using System.Web.Http;
using Owin;

namespace Enterprisecoding.OwinUI {
    internal class Startup {
        public void Configuration(IAppBuilder app) {
            var config = new HttpConfiguration();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            app.UseWebApi(config); 

            app.UseFileServer(options => options.WithRequestPath("/")
                                                .WithPhysicalPath("StatikIcerik")
                                                .WithDirectoryBrowsing());

        }
    }
}

  Yukarıdaki kodla birlikte uygulamamızın statik dosyaları StatikIcerik klasöründe arayacağını söylememe gerek yok sanırım… Dolayısıyla projemizde bu isimde bir klasör oluşturalım.

   Bu adımlar sonrasında sıra geliyor Ana formumuza. Ana form üzerine WebBrowser kontrolünü tüm formu kaplayacak şekilde atabilirsiniz. Bu noktada; .Net ile birlikte gelen WebBrowser kontrolü açıkçası çok yetersiz kaldığı ve çok hata çıkarttığı için doğrusu benim tercihim değil. Ben bunun yerine arkaplanda Chrome motorunu kullanan Awesomium’u kullanmayı tercih ettim. Ticari olmayan ya da küçük ölçekli firmalar için ücretsiz olan bu bileşen aynı zamanda Awesomium.Net sayesinde C# desteğine de sahip… Tercih sizin…

   Ana ekranım açılır açılmaz browser kontrolünün sunucumun ana sayfasını açmasını sağladım. Tabi bunun yanında açılan sayfanın başlığı değiştiğinde aynı şekilde ana formumum başlığını da güncelledim. Benzer şekilde bir favicon varsa bunu ana formun ikonu haline getirdim. Bu sayede masaüstü uygulamamın koduna dokunmadan sunduğum web uygulamasını doğal bir parçasıymış gibi gösterebildim…

   Şimdi sıra geldi web uygulamasına. Web uygulaması olarak son zamanların giderek yükselen trendi olan tek sayfa web uygulaması (Single-Page Application, SPA) konseptini seçtim. Uygulamam aslında basit bir uygulama olacak; x bir veri kaynağından gelen yapılacak işler listesini kullanıcıya göstererek işlem yapması sağlayacak… Yani bir To-Do uygulaması…

   Öncelikle ASP.Net Web API üzerinden yapılacak işlerin çekileceği bir controller hazırladım: YapilacaklarController;

using System.Collections.Generic;
using System.Web.Http;

namespace Enterprisecoding.OwinUI.Controllers {
    public class YapilacaklarController : ApiController {
        public IEnumerable<Yapilacak> Get() {
            return new[] {
                new Yapilacak { Id = 1, Content = "OWIN makaleleri  yaz", Tamamlandi = true }, 
                new Yapilacak { Id = 2, Content = "Roslyn scripting örneği kodla", Tamamlandi = true },
                new Yapilacak { Id = 3, Content = "Geri bildirimleri yanıtla", Tamamlandi = false },
                new Yapilacak { Id = 4, Content = "Tatilin tadını çıkar", Tamamlandi = false }
            };
        }
    }

    public class Yapilacak {
        public int Id;
        public string Content;
        public bool Tamamlandi;
    }
}

   Ana amacımız bu konsepti anlamak olduğu için gerçek bir veri kaynağına bağlanıp veri çekmek yerine sabit veri döndüm; ama gerçek bir uygulamada tabi ki veri kaynağı kullanılacaktır. Örnek olması adına sadece 4 yapılacak işi listeledim. İşin tamamlanıp tamamlanmadığını belirtmek adına da Tamamlandi alanını ekledim. Daha da ilerlemeden bir tarayıcı açarak http://localhost:8080/api/yapilacaklar adresine gidecek olursak aşağıdaki JSON çıktısını görebilirsiniz;

[
 {"Id":1,"Content":"OWIN makaleleri  yaz","Tamamlandi":true},
 {"Id":2,"Content":"Roslyn scripting örneği kodla","Tamamlandi":true},
 {"Id":3,"Content":"Geri bildirimleri yanıtla","Tamamlandi":false},
 {"Id":4,"Content":"Tatilin tadını çıkar","Tamamlandi":false}
]

  Şimdi, sıra geldi bu veriyi kullanacağımız html sayfasına. Öncelikle veriyi çekebilmek için JQuery kullandım; çünkü JQuery ile oldukça rahat bir şekilde ajax çağrıları yapıp gelen JSON yanıtını javascript nesnelerine dönüştürebiliyoruz. Html sayfasını StatikIcerik klasörü altında açtım, adına index.html dedim. Bu sayede varsayılan döküman olarak istemciye dönülebilecek. Sonrasında da her derleme çıktı klasörüne kopyalanmasını sağladım. Yapılacakların listesini çekip bunu sayfada gösterebilmek adına aşağıdaki basit iki javascript fonksiyonunu yazdım;

function YapilacaklariGetir() {
    $.ajax({
        url: 'http://localhost:8080/api/yapilacaklar',
        type: 'GET',
        dataType: 'json',
        success: function (data) {
            YapilacaklariYaz(data);
        },
        error: function (x, y, z) {
            alert(x + '\n' + y + '\n' + z);
        }
    });
}

function YapilacaklariYaz(yapilacaklar) {
    var sonuc = "<ul>";
    $.each(yapilacaklar, function (index, yapilacak) {
        sonuc += "<li>" + yapilacak.Content + "</li>";
    });

    sonuc += "</ul>";
    $("#divSonuc").html(sonuc);

}

  Son olarak da sayfaya bir buton ekleyip, tıklandığında YapilacaklariGetir fonksiyonunun çalıştırılmasını sağladım. Yani, toparlayacak olursak, aşağıdaki gibi bir web sayfası yazdım;

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Enterprisecoding - OWIN Arayüz Örneği</title>
    <script src="http://code.jquery.com/jquery-2.0.0.min.js"></script>
</head>
<body>
    <script type="text/javascript">
        function YapilacaklariGetir() {
            $.ajax({
                url: 'http://localhost:8080/api/yapilacaklar',
                type: 'GET',
                dataType: 'json',
                success: function (data) {
                    YapilacaklariYaz(data);
                },
                error: function (x, y, z) {
                    alert(x + '\n' + y + '\n' + z);
                }
            });
        }

        function YapilacaklariYaz(yapilacaklar) {
            var sonuc = "<ul>";
            $.each(yapilacaklar, function (index, yapilacak) {
                sonuc += "<li>" + yapilacak.Content + "</li>";
            });

            sonuc += "</ul>";
            $("#divSonuc").html(sonuc);

        }
    </script>
    <button onclick="YapilacaklariGetir();return false;">Yapilacaklari Listele</button>
    <div id="divSonuc"></div>
</body>
</html>

   Ok, sıra geldi uygulamayı çalıştırmaya. F5 ile uygulamamızı çalıştıracak olursak aşağıdaki gibi bir ekran bizi karşılayacaktır;

Yapılacaklar uygulamamız ilk açıldığında henüz bir veri gösterilmiyor

 

   Biliyorum. İçerik olarak çok boş geldi 🙂 Bir de Yapılacakları Listele butonuna basınca bakın bakalım;

Yapılacaklar uygulamamızın Web API controller'dan çektiği veriler

   Taa.. Taaammm… Bir ajax çağrısı ile controller üzerinden veri çekilerek kullanıcıya sunuldu.. Üstelik masaüstü uygulaması deneyimiyle…

   Biliyorum; arayüz size fazla sade geldi.. O zaman bira css ile bakalım bu sadeliği dağıtabilecekmiyiz;

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="utf-8" />
        <title>Enterprisecoding - OWIN Arayüz Örneği</title>
        <script src="http://code.jquery.com/jquery-2.0.0.min.js"></script>
    </head>
    <style>
        /* http://meyerweb.com/eric/tools/css/reset/ 
   v2.0 | 20110126
   License: none (public domain)
 */

html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {
    margin: 0;
    padding: 0;
    border: 0;
    font-size: 100%;
    font: inherit;
    vertical-align: baseline;
}

article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block }

body { line-height: 1 }

ol, ul { list-style: none }

blockquote, q { quotes: none }

blockquote:before, blockquote:after, q:before, q:after {
    content: '';
    content: none;
}

table {
    border-collapse: collapse;
    border-spacing: 0;
}

/* * Copyright (c) 2012 Thibaut Courouble
 * http://www.webinterfacelab.com
 *
 * Licensed under the MIT License:
 * http://www.opensource.org/licenses/mit-license.php
   ================================================== */

/* ========================================================
   Page
   =====================================================
   ================================================== */

body {
    background: #f8f6f6;
    color: #404040;
    font-family: 'Lucida Grande', Verdana, sans-serif;
    font-size: 13px;
    font-weight: normal;
    line-height: 20px;
}

a {
    color: #1e7ad3;
    text-decoration: none;
}

a:hover { text-decoration: underline }

.container {
    margin: 50px auto;
    width: 380px;
}

/* ========================================================
   Icons
   =====================================================
   ================================================== */

[class^="icon-"], [class*=" icon-"] {
    display: inline-block;
    width: 12px;
    height: 12px;
    vertical-align: -2px;
    margin-right: 2px;
    background-image: url("http://demo.webinterfacelab.com/14-to-do-list/img/sprite.png");
    background-repeat: no-repeat;
    text-indent: 100%;
    white-space: nowrap;
    overflow: hidden;
}

.icon-check { background-position: 0 0 }

.icon-add { background-position: -12px 0 }

.icon-delete { background-position: -24px 0 }

.icon-settings { background-position: -36px 0 }

.icon-previous { background-position: -48px 0 }

.icon-next { background-position: -60px 0 }

/* ========================================================
   Todo List
   =====================================================
   ================================================== */

.todo {
    position: relative;
    width: 260px;
    margin: 0 auto;
    padding: 12px 0;
    background: #fff;
    border: 1px solid;
    border-color: #dfdcdc #d9d6d6 #ccc;
    border-radius: 2px;
    -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
    -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}

.todo:before, .todo:after {
    content: '';
    position: absolute;
    z-index: -1;
    height: 4px;
    background: #fff;
    border: 1px solid #ccc;
    border-radius: 2px;
}

.todo:after {
    left: 0;
    right: 0;
    bottom: -3px;
    -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
    -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}

.todo:before {
    left: 2px;
    right: 2px;
    bottom: -5px;
    border-color: #c4c4c4;
    -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
    -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}

.lt-ie9 .todo:before, .lt-ie9 .todo:after {
    height: 1px;
    border-top: 0;
}

.todo-list { border-top: 1px solid #e6ebed }

.todo-list:before {
    content: '';
    width: 3px;
    z-index: 2;
    border: 1px solid #f2e3df;
    border-width: 0 1px;
    position: absolute;
    top: 0px;
    bottom: 0px;
    left: 35px;
}

.todo-list li {
    position: relative;
    padding: 7px 15px 7px 50px;
    line-height: 21px;
    font-size: 12px;
    color: #8b8f97;
    border-bottom: 1px solid #e6ebed;
}

input[type=checkbox] {
    display: none;
}

input[type=checkbox]:checked + .toggle:after{
    opacity: 1;
}

.todo-list .toggle {
    display: block;
    height: 35px;
    width: 35px;
    position: absolute;
    top: 0px;
    bottom: 0px;
    left: 0px;
    text-indent: 100%;
    overflow: hidden;
    cursor: pointer;
}

.toggle:after {
    content: '';
    position: absolute;
    position: absolute;
    width: 7px;
    height: 3px;
    background: transparent;
    top: 14px;
    left: 13px;
    border: 2px solid #aaa;
    border-top: none;
    border-right: none;

    -webkit-transform: rotate(-45deg);
    -moz-transform: rotate(-45deg);
    -o-transform: rotate(-45deg);
    -ms-transform: rotate(-45deg);
    transform: rotate(-45deg);
    opacity: 0;
}

.todo-list .toggle:before {
    content: '';
    width: 15px;
    height: 15px;
    background: #faf9f9;
    border: 1px solid #6bb3ca;
    border-radius: 2px;
    position: absolute;
    top: 9px;
    left: 9px;
    -webkit-box-shadow: 0 1px 1px #dfecf4;
    -moz-box-shadow: 0 1px 1px #dfecf4;
    box-shadow: 0 1px 1px #dfecf4;
}

.todo-list .toggle:hover:before {
    -webkit-box-shadow: 0 0 3px #6bb3ca;
    -moz-box-shadow: 0 0 3px #6bb3ca;
    box-shadow: 0 0 3px #6bb3ca;
}

.todo-list .done .toggle:before, .todo-list .toggle:active:before {
    border-color: #c0c0c0 #ccc #d8d8d8;
    -webkit-box-shadow: inset 0 1px rgba(0, 0, 0, 0.05), inset 0 5px 5px rgba(0, 0, 0, 0.05);
    -moz-box-shadow: inset 0 1px rgba(0, 0, 0, 0.05), inset 0 5px 5px rgba(0, 0, 0, 0.05);
    box-shadow: inset 0 1px rgba(0, 0, 0, 0.05), inset 0 5px 5px rgba(0, 0, 0, 0.05);
}
.todo-controls {
    margin: 0 15px 12px 50px;
    height: 12px;
}

.todo-controls li { float: left }

.todo-controls li + li { margin-left: 10px }

.todo-controls .right { float: right }

.todo-controls a {
    display: block;
    margin: 0;
    opacity: .6;
}

.todo-controls a:hover { opacity: 1 }

.todo-pagination {
    margin: 12px 12px 0 50px;
    height: 22px;
}

.todo-pagination li { float: left }

.todo-pagination .next { float: right }

.todo-pagination .next i { margin: 0 0 0 2px }

.todo-pagination a, .todo-pagination span {
    display: block;
    line-height: 22px;
    font-size: 11px;
    color: #676f7f;
}

.todo-pagination a {
    padding: 0 8px;
    text-shadow: 0 1px #fff;
    background: #f1f0f0;
    border-radius: 3px;
}

.todo-pagination a:hover {
    background: #e9e9e9;
    text-decoration: none;
}

.todo-pagination span {
    padding: 0 4px;
    opacity: .3;
}

/* ========================================================
   About
   =====================================================
   ================================================== */

.about {
    margin: 80px auto 50px;
    padding: 15px 20px;
    width: 300px;
    text-align: center;
    color: #777;
    text-shadow: 0 1px rgba(255, 255, 255, 0.7);
    background: rgba(0, 0, 0, 0.05);
    border-radius: 3px;
}

.about a {
    padding: 1px 3px;
    margin: 0 -1px;
    color: #1c74c8;
    text-decoration: none;
    border-radius: 2px;
}

.about a:hover {
    color: #fff;
    text-shadow: 0 1px #0063A6;
    background: #0090D2;
}

.links { zoom: 1 }

.links:before, .links:after {
    content: "";
    display: table;
}

.links:after { clear: both }

.links a {
    padding: 6px 0;
    float: left;
    width: 50%;
    font-size: 14px;
}

.author {
    margin-top: 15px;
    font-size: 11px;
}
    </style>
<body>
    <script type="text/javascript">
        function YapilacaklariGetir() {
            $.ajax({
                url: 'http://localhost:8080/api/yapilacaklar',
                type: 'GET',
                dataType: 'json',
                success: function (data) {
                    YapilacaklariYaz(data);
                },
                error: function (x, y, z) {
                    alert(x + '\n' + y + '\n' + z);
                }
            });
        }

        function YapilacaklariYaz(yapilacaklar) {
            var sonuc = "";
                 
            $.each(yapilacaklar, function (index, yapilacak) {
                sonuc += yapilacak.Tamamlandi ?
                    "<li class='done'><input type='checkbox' id='todo" + yapilacak.Id + "' checked disabled />" :
                    "<li><input type='checkbox' id='todo" + yapilacak.Id + "'/>";
                sonuc += "<label class='toggle for='todo" + yapilacak.Id + "'></label>" + yapilacak.Content + "</li>";
            });

            $(".todo-list").html(sonuc);

        }
    </script>
    <div class="container">
    <section class="todo">
        <ul class="todo-controls">
            <li><a href="javascript:void(0);" class="icon-add">Add</a></li>
            <li><a href="javascript:void(0);" class="icon-delete">Delete</a></li>
            <li class="right"><a href="javascript:YapilacaklariGetir();" class="icon-settings">Settings</a></li>
        </ul>
    
        <ul class="todo-list"></ul>
        
        <ul class="todo-pagination">
            <li class="previous"><span><i class="icon-previous"></i> Geri</span></li>
            <li class="next"><a href="javascript:void(0);">İleri <i class="icon-next"></i></a></li>
        </ul>
    </section>
</div>
</body>
</html>

  Aslına bakarsanız bu css’ler bana ait değil ve orjinalini http://cssdeck.com/labs/simple-css-todo-list adresinde bulabilirsiniz. Bu css’i uyguladığımda sonuç nasıl mı oldu? Kendi gözlerinizle görün;

Yapılacaklar uygulamamız, css uygulanmış hali

Yapılacaklar uygulamamız, css uygulanmış haliyle veriler geldiğinde...

Yapılacaklar uygulamamız, css uygulanmış haliyle...

   Çok daha güzel değil mi! CSS’le biraz daha oynayarak çok daha başarılı bir arayüz deneyimi sunmanız da mümkün. İşin güzel tarafı, bir web uygulaması sunuyorsanız zaten ekibinizde bu konuda büyük işler çıkartabilecek kişiler mevcuttur… Web deneyimi olan ekibinizin masaüstü uygulaması geliştirmesine gerek yok! Sıfırdan kodlamaya gerek yok!

   Daha iyi bir arayüz tasarımı ile web uygulamalarını masaüstüne taşımak daha başarılı olacaktır. Üstelik arayüz css’inde yapacağınız basit oynamalarda farklı genişliklere adapte olan bir uygulama başarınızı daha da arttıracaktır.

Yönetici paneli örneği (küçük ekranda)

Yönetici paneli örneği (orta büyüklükteki ekranda)

 

Yönetici paneli örneği (büyük ekranda)

 

   Konsepti anlatabilmek adına paylaştığım yukarıdaki ekran görüntülerinde yer alan şablon tahmin edebileceğiniz gibi bana ait değil. İnternet üzerinden satın aldığım ve aslında bir web uygulamasının yönetim paneli arayüzü için tasarlanmış bir şablon. Gördüğünüz gibi doğru bir arayüzle harika bir masaüstü uygulamasına dönüştü.

Fatih Boy

Ankara'da yaşayan Fatih, bir kamu kurumunda danışman olarak çalışmaktadır. ALM süreçleri, kurumsal veri yolu sistemleri, kurumsal altyapı ve yazılım geliştirme konularında destek vermektedir. Boş zamanlarında açık kaynak kodlu projeler geliştirmeyi ve bilgisini yazdığı makalelerle paylaşmayı seven Fatih, aynı zamanda Visual C# ve Visual Studio teknolojileri konusundan Microsoft tarafından altı yıl üst üste MVP (En Değerli Profesyonel) ödülüne layık görülmüştür. İş hayatı boyunca masaüstü uygulamaları, web teknolojileri, akıllı istemciler gibi konularda Asp.Net, Php, C#, Java programlama dilleri ve MySql, MsSql ve Oracle gibi veritabanı yönetim yazılımları ile çalışmıştır. İngilizce ve Türkçe olarak yayınlanan makalelerini gerek İngilizce bloğunda, gerekse de Türkçe bloğunda bulabileceğiniz gibi web sitesinden de açık kaynak kodlu geliştirdiği yazılımlarına ulaşabilirsiniz. vCard - Twitter - Facebook - Google+

1 Yorum

  1. Emre   •  

    Hocam statik dosya erişimi dışında bir artı sağlamadı gibi düşünüyorum normalde de web api’yi sunucuya yükleyip form app’e browser gömerek bu işlemleri yapıyorduk yanlış mıyım ?

Bir Cevap Yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir