воскресенье, 21 декабря 2008 г.

Еще про log4net

В комментариях к прошлой статье про log4net задал мне товарищ следующий вопрос:

Вопрос: ты знаешь, как заставить log4net создавать лог-файлы для каждого экземпляра интерфейса ILog?
Т.е. есть у меня
static log4net.ILog log = log4net.LogManager.GetLogger("My_mega_logger");
Теперь я хочу, чтобы лог-файл назывался "log_My_mega_logger.log".

Я подумала-подумала, да и придумала решение. Заодно, кажется, разгадала секрет, зачем в конфиге нужен элемент logger. Итак, вот что получилось.

Для примера я создаю консольное приложение, и цепляю в References библиотеку log4net.

App.config

Добавляем в проект конфигурационный файл и пишем в нем, например, такое

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
  </configSections>
  <log4net debug="true">
    <appender name="SomeAppender" type="log4net.Appender.RollingFileAppender">
      <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
      <file value="Logs\some.log" />
      ...
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%-5p [%d] [%M] %m%n" />
      </layout>
    </appender>
    <appender name="OtherAppender" type="log4net.Appender.RollingFileAppender">
      <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
      <file value="Logs\other.log" />
      ...
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%-5p [%d] [%M] %m%n" />
      </layout>
    </appender>
    <root>
      <level value="ALL" />
    </root>
    <logger name="SomeLogger">
      <level value="ALL" />
      <appender-ref ref="SomeAppender" />
    </logger>
    <logger name="OtherLogger">
      <level value="ALL" />
      <appender-ref ref="OtherAppender" />
    </logger>
  </log4net>
  <appSettings>
    <add key="log4net.Internal.Quiet" value="true" />
  </appSettings>
</configuration>

Т.е. мы определили два Appender-а, которые завязаны на разные файлы, и два Logger-а, которые по-разному называются и завязаны на соответствующие Appender-ы.

В приложении

Класс нашего консольного приложения может выглядеть так:

class Program
{
  private static readonly ILog someLog = LogManager.GetLogger("SomeLogger");
  private static readonly ILog otherLog = LogManager.GetLogger("OtherLogger");

  static void Main(string[] args)
  {
    XmlConfigurator.Configure();

    someLog.Debug("Some message");
    otherLog.Debug("Other message");
  }
}

Немного по-индийски, но, в общем, работает. Когда программа отработает, одно сообщение запишется в один файл, другое - в другой.

вторник, 18 ноября 2008 г.

Доступ к функциям телефона из web-страницы

Например, добавили мы на наш сайт некий платный сервис. Для доступа к нему нужно послать SMS с текстом таким-то на номер такой-то. Однако, на наш сайт ходят люди с телефонов, и им, для того, чтобы этим воспользоваться, надо закрыть браузер, написать SMS (еще вспомнить, что там вообще писать надо), а потом открыть браузер обратно. Получается довольно-таки неудобно.

Однако, есть выход. Оказывается, существует такая полезная вещь, как Trigger phone call. Суть в том, что можно вставлять прямо в HTML-код гиперссылки со специальным URI, и по нажатию на эти ссылки будут отправляться SMS, MMS, совершаться звонки и т.д.

Примеры

Отправка SMS

Ссылка, открывающая редактор SMS с текстом "Hello" и с указанным адресатом:

<a href="sms:+79021234567?body=Hello">Click to send message</a>

Ссылка, открывающая редактор SMS с текстом "Hello" и с указанным списком номеров:

<a href="sms:+79021234567,+79081234567?body=Hello">Click to send message</a>

Пишут, что в URI вместо sms: можно использовать smsto:, SMS: (для телефонов Siemens) и даже com.nokia.sms: (для телефонов Nokia).

Совершение вызова

Ссылка, совершающая вызов:

<a href="tel:+79021234567">Call</a>

Кстати, из Opera Mini, и без ссылок можно звонить на все числа, которые есть на сайте. Для дефаултного браузера же подобная конструкция очень может пригодиться.

Отправка MMS

Ссылка, отправляющая MMS:

<a href="mmsto:+3581234567?subject=Hi?body=Nice%20to%20see?">Send message</a>

Большой минус

Это все, конечно, очень хорошо и полезно. Но проблема в том, что не на всех телефонах работает. У меня были в наличии Nokia 2600 classic (самый обычный телефон) и Nokia же N73 (смартфон). Получилась такая картина:

Nokia 2600 classic (Series 40):

Default browserOpera Mini
SMSРугается на неверный адресМолча не работает
MMSРугается на неверный адресМолча не работает
telРаботаетРаботает

Nokia N73 (Series 60):

Default browserOpera Mini
SMSРаботает, если URI cодержит sms, но не работает с smsto (не заполняется тело сообщения) Работает smsto, но не работает sms (не заполняется тело сообщения)
MMSВроде работаетТоже вроде работает
telРаботаетРаботает

Эмулятор Android

Про него я сначала и забыла, но решила восполнить этот пробел.

Default browser
SMSИ sms, и smsto открывают редактор SMS, но адресат и тело сообщения не заполнены.
MMSОткрывается редактор, но, опять же, ничего не заполнено.
telРаботает

Зато я узнала, что эмулятор можно поворачивать на 90 градусов с помощью сочетания Ctrl+F11

В заключение

Итак, конструкция полезная и удобная, но использовать ее следует осторожно, потому как работать может не везде.

Для интересующихся: можно зайти телефоном на специальную страничку, потыкать по всем ссылкам, о которых шла речь, и посмотреть, какие из них будут работать.

Ссылки

четверг, 13 ноября 2008 г.

Программа для отображения состояния коннекта

Переехала наша компания в другой офис. Неделю уже там живем, а интернет от распрекрасной Дальсвязи все такой же - то потухнет, то погаснет. А сервера еще не переехали, так что к базам и прочим нужным штукам мы стучимся тоже по интернету, а вовсе не через локалку. Так что картина получается грустная.

Так что приходилось располагать окошки так, чтобы видеть консоль с ping -t google.ru. А потом мне пришла в голову светлая идея написать программу для отображения состояния пинга в трее. В качестве языка реализации был выбран C#, дабы не заморачиваться.

Сначала создаем проект вида Windows Application. На главную форму можно не кидать никакие элементы управления, все равно не понадобятся. Зато в ресурсы надо добавить два каких-нибудь значка, один из которых будет называться Ping и обозначать коннект, а второй - Noping и отсутствие коннекта. Вот код формы:

namespace Pinger
{
  public partial class PingForm : Form
  {
    /// <summary>Иконка, обозначающая коннект</summary>
    private static NotifyIcon pingIcon;
    /// <summary>Иконка, обозначающая отсутствие коннекта</summary>
    private static NotifyIcon nopingIcon;

    /// <summary>true, если есть коннект, false иначе</summary>
    private static bool pingState;
    public static bool PingState
    {
      get { return pingState; }
      set
      {
        if (pingState != value) // если значение pingState изменилось
        {
          pingState = value;
          
          // переключаем иконки в трее
          NotifyIcon currentIcon = pingState ? pingIcon : nopingIcon;
          pingIcon.Visible = pingState;
          nopingIcon.Visible = !pingState;
          
          // показываем подсказку
          currentIcon.ShowBalloonTip(2000);
        }
      }
    }

    /// <summary>
    /// Функция, отправляющая пинг
    /// </summary>
    private void MakePing()
    {
      Ping pingSender = new Ping();

      // Добавляем обработчик для события окончания пинга
      pingSender.PingCompleted += new PingCompletedEventHandler(PingCompletedCallback);
      
      // инициализируем буфер с данными
      string data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
      byte[] buffer = Encoding.ASCII.GetBytes(data);

      // куда посылаем пинг
      string who = "google.ru";

      // сколько ждем
      int timeout = 3000;

      AutoResetEvent waiter = new AutoResetEvent(false);
      PingOptions options = new PingOptions(64, true);

      while (true)
      {
        try
        {
          // собственно посыл пинга
          pingSender.SendAsync(who, timeout, buffer, options, waiter);
        }
        catch (Exception e)
        {
        }

        Thread.Sleep(timeout);
      }
    }

    /// <summary>
    /// Конструктор формы
    /// </summary>
    public PingForm()
    {
      InitializeComponent();

      System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(PingForm));

      // читаем из ресурсов иконки
      pingIcon = new NotifyIcon();
      pingIcon.Icon = ((System.Drawing.Icon)(resources.GetObject("Ping")));
      pingIcon.BalloonTipText = "Ping is OK";
      pingIcon.BalloonTipTitle = "Pinger";
      pingIcon.BalloonTipIcon = ToolTipIcon.Info;

      nopingIcon = new NotifyIcon();
      nopingIcon.Icon = ((System.Drawing.Icon)(resources.GetObject("Noping")));
      nopingIcon.BalloonTipText = "No Ping";
      nopingIcon.BalloonTipTitle = "Pinger";
      nopingIcon.BalloonTipIcon = ToolTipIcon.Warning;

      // значение по умолчанию
      PingState = true;

      // запускаем функцию MakePing в отдельном потоке
      Thread t = new Thread(new ThreadStart(MakePing));
      t.Start();
    }

    /// <summary>
    /// Обработка результата пинга
    /// </summary>
    public static void PingCompletedCallback(object sender, PingCompletedEventArgs e)
    {
      // пинг считается правильным, если не возникло ошибок и пришел ответ Success
      PingState = e.Error == null && e.Reply.Status == IPStatus.Success;
    }
  }
}

Вот, что получилось:

Появился коннект:

Есть коннект

Пропал коннект:

Нет коннекта

Я лично сейчас смотрю и очень радуюсь :) Правда, код для закрытия я поленилась писать, и закрыть программу можно только, отстрелив процесс в Task Manager.

Ссылки

среда, 12 ноября 2008 г.

Mobile Processing

Думала, мой следующий пост (вернее, следующая серия) будет опять про Андроид, ан нет :) Недавно попалась мне одна удивительная штука под названием Mobile Processing. Это инструмент, облегчающий процесс создания мобильных приложений. Обычно, если хочется написать мобильное приложение, надо учить Java, разбираться, как устроен мидлет, какие у него там события, каков жизненный цикл. И, если честно, не могу сказать, что копаться в этом очень приятно.

Другое дело Mobile Processing. Тут вообще необязательно что-либо знать про мидлеты. Есть собственный простой язык, похожий на Java, с помощью которого можно описать логику приложения. Есть IDE, которая выглядит таким образом:

Единственное, что надо настроить в IDE - указать путь к WTK.

После этого мы пишем код, в котором просто определяем функции типа setup(), draw() и т.п., сохраняем все это дело в файл %progname%.PDE потом нажимаем кнопку "Пыщь" - и среда генерит нам jar.

Поясню на примере. Вот программа, написанная за пару минут и позволяющая двигать кружочек по полю, заполненному желтым фоном:

int x, y;
int r = 5;

void setup()
{
 x = 100;
 y = 100;
}

void draw()
{
 background(255, 204, 0);
 ellipse(x, y, 2 * r, 2 * r);switch (keyCode){
  case UP: y--; break;
  case DOWN: y++; break;
  case LEFT: x--; break;
  case RIGHT: x++; break;
  default: break;}
}

void keyReleased()
{
 keyCode = 0;
}

Видно, что код достаточно простой и понятный. Кроме того, если такую программу честно писать на Java, времени уйдет гораздо больше.

Полученный jar-файл весил 46 Кб, но с помощью имеющегося оптимизатора-обфускатора ProGuard его удалось ужать до 7 Кб. Программа запросто запустилась на моей Nokia 2600c, а также на Nokia N73.

В общем, данная тулза привела меня в восторг своей простотой (особенно после монстров типа NetBeans или IDEA, которую все хвалят, но в которой мне так и не удалось создать Mobility-проект). Позиционируется она как средство быстрого создания прототипов приложений, но, честно говоря, возможности у нее не такие уж и слабые. Есть библиотеки для работы с Bluetooth, с камерой и другими возможностями телефона. И самое интересное - оно работает. Единственное, что меня удивляет, так это практически отсутствие упоминаний о данном продукте в рунете. Вроде, и проект не совсем мертвый (последний релиз в июле 2008). Интересно, почему бы это?

Но я в ней еще поковыряюсь, дабы узнать, что там плохо или хорошо. Пока что все обнаруженные минусы относятся к редактору: отсутствие отладчика и IntelliSense, неидеальный keymapping (я люблю, чтобы работали Ctrl+Ins и Shift+Ins, а там только Ctrl+C и Ctrl+V. Хотя, возможно, это настраивается в конфиге, который там далеко спрятан).

Ссылки по теме

четверг, 16 октября 2008 г.

Знакомство с Android. Часть 1: Простое приложение для Android

Недавно заинтересовала меня платформа Android. Как-то много говорят о нем в последнее время, да и вообще хотелось узнать, такая же ли там ужасная Java, как в мидлетах. Так что потратила я некоторое время на копание в нем, написала простое приложение, и сейчас вот буду делиться опытом.

UPDATE

Серия статей по игре Life обновлена 03.11.2010. Исправлены ошибки и проведена адаптация под Android версии 2.2. Всем спасибо за замечания и дополнения!

Постановка задачи

Первым нашим приложением для Android будет реализация всем известной игры Life. Местом дейтвия будет прямоугольное клеточное поле, размеры которого запрашиваются у пользователя. Также у пользователя запрашивается начальное количество клеток. Первое поколение расставляется по карте случайным образом. Последующие поколения получаются по следующим правилам:
  • Если у живой клетки меньше двух или больше трёх соседей, то она погибает.
  • Если у пустой клетки ровно три соседки, она оживает.
Все входные параметры должны проверяться на правильность: столбцов должно быть не меньше 5 и не больше 25, строк должно быть не меньше 5 и не больше 35, начальное количество клеток должно быть не больше, чем ячеек на поле. Для реализации поля будет использован класс GridView.
Для разработки была использована среда Eclipse и Android plugin для неё.
Статья будет из четырех частей:
Итак, начнём.

В этой части

Мы создадим проект, рассмотрим его структуру и напишем простое приложение, состоящее из одной формы. На форме будет интерфейс для ввода данных и кнопка Run.

Создание и обзор проекта

На установке Android SDK и плагина для Eclipse останавливаться не будем, т.к. это достаточно подробно описано в официальном мануале. Создаем в Eclipse новый Android Project:
Создание нового проекта под Android
После нажатия на кнопку Finish создастся новый проект с такой структурой файлов:
Структура проекта
Рассмотрим эту структуру внимательнее.

/res/drawable-dpi

Сюда помещаются все графические файлы, используемые в приложении, для разных разрешений экрана. На данный момент там есть только файл icon.png - главная иконка приложения.

/res/layout

В эту папку помещаются файлы, в которых в формате XML описывается внешний вид форм, расположение контролов и т.д. (как dfm-ки в Дельфи). Плагин даже создал разметку для нашей единственной формы и назвал её main.xml. Позже мы рассмотрим ее подробнее.

/res/values

В этой папке хранятся общие константы для всего приложения, как то: текст, используемый элементами управления, цвета, стили и т.д.. Например, если мы хотим вывести "Hello World" в TextView, можно это сделать явно в разметке, как мы всю жизнь делали в тех же dfm-ках или aspx; либо создать в strings.xml константу hello со значением "Hello World", после чего пойти обратно в разметку и в атрибутах этого TextView прописать android:text="@string/hello".

/gen/R.java

Это такой специальный сгенерированный класс, посредством которого осуществляется доступ к ресурсам приложения (т.е. ко всему тому, что есть в папке res). Например, R.string.hello возвращает константу с именем hello из strings.xml.

StartActivity.java

Это нам плагин сгенерировал класс для главной (и пока что единственной) формы приложения. Там пока содержится единственный обработчик onCreate, и написано там только setContentView(R.layout.main);. С помощью этой строчки к данной форме привязывается разметка, описанная в файле /res/layout/main.xml

AndroidManifest.xml

В этом файле перечисляются общие свойства проекта (версия, package и прочее), а также все формы (Activities), входящие в проект.

Разметка формы (Layout)

Элементы управления в Android называются Views и наследуются от класса View или ViewGroup. Класс ViewGroup также унаследован от View, но его отличие в том, что в него могут быть вложены другие View или ViewGroup.
Иерархия View
Плагин создал простейшую разметку для нашей единственной формы (main.xml):

main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="@string/hello"
    />
</LinearLayout>
Вначале задаётся Layout, т.е. правило, согласно которому элементы управления следуют друг за другом. LinearLayout значит, что они идут друг за другом сверху вниз (android:orientation="vertical"). Бывают и другие Layout-ы: TableLayout, с помощью которого можно выстроить контролы в таблицу; FrameLayout, который ставит контролы один на другой; и т.д.
Мы воспользуемся TableLayout
Сделаем вот такую разметку:

main.xml

<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:stretchColumns="1"
    android:padding="10dip"
    >
    <TableRow android:paddingBottom="5dip">
        <TextView
            android:text="@string/columns_title"
            android:paddingRight="10dip"
            android:gravity="right"
            android:textStyle="bold"
        />
        <EditText
            android:id="@+id/columns_count"
            android:text="25"
            android:inputType="number"
        />
    </TableRow>
    <TableRow android:paddingBottom="5dip">
        <TextView
            android:text="@string/rows_title"
            android:paddingRight="10dip"
            android:gravity="right"
            android:textStyle="bold"
        />
        <EditText
            android:id="@+id/rows_count"
            android:text="35"
            android:inputType="number"
        />
    </TableRow>
    <TableRow android:paddingBottom="5dip">
        <TextView
            android:text="@string/cells_title"
            android:paddingRight="10dip"
            android:gravity="right"
            android:textStyle="bold"
        />
        <EditText
            android:id="@+id/cells_count"
            android:text="100"
            android:inputType="number"
        />
    </TableRow>
    <TableRow>
        <Button
            android:id="@+id/run"
            android:text="@string/run_title"
            android:textStyle="bold"
            android:layout_span="2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
        />
    </TableRow>
</TableLayout>
В файл res/values/strings.xml при этом нужно добавить следующие строки:

strings.xml

<string name="run_title">Run!</string>
<string name="columns_title">Columns:</string>
<string name="rows_title">Rows:</string>
<string name="cells_title">Cells:</string>
Форма при этом будет выглядеть так:
StartActivity
Рассмотрим некоторые атрибуты, использованные в разметке

android:id

Идентификатор элемента. Если он указан, то в дальнейшем его можно найти на форме с помощью метода findViewById(id). Для контролов, которых мы не планируем в дальнейшем трогать (например, для заголовков), можно это свойство и вовсе не указывать. Идентификаторы можно складывать в файл ids.xml, но вместо этого обычно применяется синтаксис @+id/View1. Это означает, что идентификатор View1 добавляется в константы прямо на ходу. В R.java соответствующие поля также добавляются автоматически.

android:layout_width и android:layout_heigth

Свойства layout_width и layout_heigth обозначают, какую часть родительского контрола будет занимать данный элемент управления: всю (fill_parent или match_parent, начиная с 2.2) или ровно столько, сколько требуется (wrap_content). Можно также задавать численные значения.

android:inputType

Это атрибут EditText. С его помощью можно устанавливать формат вводимого значения. Есть ряд предопределенных форматов (date, phone, etc). Значение number значит, что в это поле можно вводить только целые положительные числа.

android:gravity, android:layout_gravity

Устанавливает выравнивание в данном элементе управления. Отличие в том, что gravity задает выравнивание дочерних контролов, а layout_gravity задает выравнивание самого контрола.

Заключение

Итак, мы создали проект для Android, рассмотрели его структуру, составили разметку для нашей единственной формы.
Исходники примера

Знакомство с Android. Часть 2: Переходы между формами

Итак, продолжим. В этой части мы добавим в проект ещё одну форму и будем открывать её по нажатию кнопки Run. Также сделаем так, чтобы параметры, которые были введены в первой форме, передавались во вторую (они там ещё пригодятся). Однако, ничего страшного мы пока с ними делать не будем, а просто напишем "Введены такие-то числа"

Знакомство с Android. Часть 3: Использование диалогов

В этой части мы сделаем проверку вводимых параметров по следующим правилам:
  • Число столбцов должно быть не меньше 5 и не больше 25.
  • Число строк должно быть не меньше 5 и не больше 35.
  • Начальное количество клеток должно быть не больше, чем ячеек на поле.
Если какое-то из этих условий не выполняется, будем выводить соответствующее предупреждение
Кроме того, мы добавим кнопку Close, при нажатии на которую приложение будет закрываться, спрашивая вначале согласие пользователя.

Знакомство с Android. Часть 4: Использование GridView

Итак, в нашем приложении осталось всего ничего: реализовать собственно алгоритм игры Life и отобразить его в GridView. Этим-то мы сейчас и займёмся.

четверг, 7 августа 2008 г.

URL rewriting в ASP.NET

URL rewriting - это такая штука, которая позволяет из некрасивых урлов с параметрами, вроде
www.somesite.ru/?cat=blogs&author=darja&year=2008&month=8
позволяет делать красивые, т.н. Человеку понятные урлы (ЧПУ), вроде
www.somesite.ru/blogs/darja/2008/8

Возникла у меня такая задача. Есть табличка (вернее, ListView), данные в которой располагаются на нескольких страницах. Пейджер у таблицы устроен так, что к N-ной странице можно обратиться http://localhost/Default.aspx?page=N И вот захотелось, чтобы к n-ной странице таблички можно было бы обратиться как http://localhost/N. Типа так ссылки приятнее выглядят и роботам хорошо. Итак, было найдено следующее решение.

URL rewriting

  1. Скачиваем библиотеку ThunderMain.URLRewriter.dll, кидаем в свой проект.
  2. В Global.asax добавляем:
    protected void Application_BeginRequest(Object sender, EventArgs e)
    {
      ThunderMain.URLRewriter.Rewriter.Process();
    }
    
  3. Вносим изменения в web.config. В configSections пишем:
    <sectionGroup name="system.web">
      <section name="urlrewrites" type="ThunderMain.URLRewriter.Rewriter, ThunderMain.URLRewriter,
     Version=1.0.783.30976, Culture=neutral, PublicKeyToken=7a95f6f4820c8dc3"/></sectionGroup>
    
    Далее, в system.web пишем:
    <urlrewrites><rule>
      <url>/(\d*)</url>
      <rewrite>Default.aspx?page=$1</rewrite></rule>
    </urlrewrites>
    
    Элементов rule может быть несколько. В узле url указывается регулярное выражение, которому должен удовлетворять url, который мы хотим переписывать, в rewrite - на что мы будем его заменять. Можно использовать стандартные для регулярных выражений группы ($1, $2 и т.д., как в перле). В нашем случае, по адресу http://localhost/N, где N - число, загрузится http://localhost/Default.aspx?page=N. Всё очень просто.
  4. Также потребуется некоторая донастройка IIS. А именно, нужно зайти в настройки сайта -> Home directory -> Configuration -> Mappings и добавить там обработчик для файлов .* такой же, как для aspx (ну там C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll). И обязательно снять галочку "Verify that file exists".
UPDATE: Кстати, у меня тут практика показала, что прописывать в IIS мэппинг для .* не есть хорошо, потому что при этом ломаются стили и прочие статические файлы, потому что IIS и их начинает спрашивать с asp.net-a. Я в итоге, дабы не париться, сделала адреса вида http://localhost/N.htm и добавила в IIS мэппинг только для .htm

Пейджер у ListView

Итак, к страницам уже можно обращаться по адресу http://localhost/N. Но в ссылки пейджера всё ещё выглядят по-прежнему. Делаем следующее.
  1. Пейджер выносим в отдельный div и ставим у него style="display:none".
  2. Под листвью добавляем панельку:
    <asp:Panel ID="Pager" runat="server" onprerender="Pager_PreRender">
    </asp:Panel>
    
  3. В коде пишем:
    protected void Pager_PreRender(object sender, EventArgs e)
    {
      Pager.Controls.Clear();
    
      DataPager pager = (DataPager)RatingListView.FindControl("RatingPager");
    
      int count = pager.TotalRowCount;
      int pageSize = pager.PageSize;
      int pagesCount = count / pageSize + (count % pageSize == 0 ? 0 : 1);
      int pageSelected = pager.StartRowIndex / pageSize + 1;
    
      for (int i = 1; i <= pagesCount; ++i)
      {
        if (pageSelected != i)
        {
          HyperLink link = new HyperLink();
          link.NavigateUrl = link.ResolveUrl(i.ToString());
          link.Text = i.ToString();
          Pager.Controls.Add(link);
        }
        else
        {
          Label label = new Label();
          label.Text = i.ToString();
          Pager.Controls.Add(label);
        }
    
        Literal space = new Literal();
        space.Text = " ";
        Pager.Controls.Add(space);
      }
    }
    
    Здесь RatingListView - наш листвью, RatingPager - его "родной" пейджер.

Литература

вторник, 29 июля 2008 г.

Не надо использовать ничего из com.sun.*

В общем, я почти дописала свой мидлет. И оказалось, что на целевой телефон (Nokia N73) он вообще отказывается ставиться. Authorisation error и все. Погуглив, узнала, что во всем виноват пакет com.sun.midp.io.Base64. Люди пишут, что вообще не стоит использовать какие-либо возможности из пакетов, которые называются com.sun.*, ибо это приводит к несовместимости мидлета с некоторыми устройствами. А так как работа с Base64 все-таки нужна, пришлось добавить в код следующее:
private static char[] base64map = new char[64];

static
{
  int i = 0;
  for (char c = 'A'; c <= 'Z'; c++)
  {
    base64map[i++] = c;
  }
  for (char c = 'a'; c <= 'z'; c++)
  {
    base64map[i++] = c;
  }
  for (char c = '0'; c <= '9'; c++)
  {
    base64map[i++] = c;
  }
  base64map[i++] = '+';
  base64map[i++] = '/';
}

private static String base64Encode(byte[] in, int offset, int length)
{
  byte[] src = new byte[length];

  for (int i = 0; i < length; ++i)
  {
    src[i] = in[offset + i];
  }
  return base64Encode(src);
}

private static String base64Encode(byte[] in)
{
  int iLen = in.length;
  int oDataLen = (iLen * 4 + 2) / 3;// output length without padding
  int oLen = ((iLen + 2) / 3) * 4;// output length including padding
  char[] out = new char[oLen];
  int ip = 0;
  int op = 0;
  int i0;
  int i1;
  int i2;
  int o0;
  int o1;
  int o2;
  int o3;
  while (ip < iLen)
  {
    i0 = in[ip++] & 0xff;
    i1 = ip < iLen ? in[ip++] & 0xff : 0;
    i2 = ip < iLen ? in[ip++] & 0xff : 0;
    o0 = i0 >>> 2;
    o1 = ((i0 & 3) << 4) | (i1 >>> 4);
    o2 = ((i1 & 0xf) << 2) | (i2 >>> 6);
    o3 = i2 & 0x3F;
    out[op++] = base64map[o0];
    out[op++] = base64map[o1];
    out[op] = op < oDataLen ? base64map[o2] : '=';
    op++;
    out[op] = op < oDataLen ? base64map[o3] : '=';
    op++;
  }
  return new String(out);
}
Теперь вместо
content = Base64.encode(src, i * CHUNK_SIZE, CHUNK_SIZE);
нужно писать
content = base64Encode(src, i * CHUNK_SIZE, CHUNK_SIZE);

вторник, 15 июля 2008 г.

Upload картинки из мидлета на сервер

Сначала следует отметить, что у мидлета есть ограничение: он может передать по HTTP файл размером не больше чем в 2Кб. Так что будем передавать файл кусочками, а потом на сервере склеивать. Кроме того, картинку следует передавать не абы как, а в кодировке base64, иначе, опять же, будут возникать непонятные проблемы передачи (по крайней мере, у меня возникали).

Клиент

Пусть мы каким-то образом получили файл как byte[]. Напишем специальный класс Uploader, с помощью которого будем заливать картинку на сервер. Передавать файл будем фрагментами по 1 Кб. Для каждого фрагмента будем открывать отдельный коннекшен и составлять POST-запрос, содержащий следующие данные:
  • Номер фрагмента (number)
  • Общее количество фрагментов (count)
  • Собственно содержание (content) в виде вложенного файла
Добавим в наш мидлет следующие поля:
/**
* Размер передаваемого фрагмента картинки
*/
private static int CHUNK_SIZE = 1024;

/**
* Граница между разделами в HTTP-запросе
*/
private static String BOUNDARY = "42hjkiqp4279h";

/**
* Адрес, на который мы посылаем запросы
*/
private static String HOST = "http://some_url/Handler.ashx";

/**
* Сообщение, которое мы получаем в случае успешной отправки запроса
*/
private static String UPLOAD_OK_MSG = "Upload OK";
И следующие методы:
/**
* Залитие картинки на сервер
* @param src Массив символов, содержащий картинку
* @return Ответ от сервера: удалось залить, или нет
*/
public String Upload(byte[] src)
{
  String response = "";
  try
  {
    int nchunks = src.length / CHUNK_SIZE;
    System.out.println("size = " + src.length + ", chunks count = " + nchunks);

    String fileId = GetNextPhotoName(5);

    for (int i = 0; i < nchunks; ++i)
    {
      String content = Base64.encode(src, i * CHUNK_SIZE, CHUNK_SIZE);
      response = UploadChunk(fileId, i + 1, nchunks, content);
      if (!IsUploadOk(response))
      {
        break;
      }
    }
    System.out.println("Uploading file " + fileId + ": " + response);
  }
  catch (Exception ex)
  {
    System.out.println(ex.getClass().getName() + " while uploading file: " + ex.getMessage());
  }
  finally
  {
  }
  return response;
}

/**
* Формирование раздела HTTP-запроса, содержащего значение некоторого параметра
* @param name Имя параметра
* @param value Значение параметра
* @return Строка - раздел HTTP-запроса
*/
private static String FormData(String name, Object value)
{
  return "--" + BOUNDARY + "\r\n" +
      "Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n" + value + "\r\n";
}

/**
* Заливает на сервер фрагмента картинки
* @param fileId Идентификатор картинки
* @param number Номер фрагмента
* @param count Общее количество фрагментов
* @param content Содержаение фрагмента
* @return Ответ от сервера: удалось залить или нет
*/  
private String UploadChunk(String fileId, int number, int count, String content)
{
  String message =
      FormData("number", new Integer(number)) +
      FormData("count", new Integer(count)) +
      FormData("sessionId", sessionId) +
      "--" + BOUNDARY + "\r\n" +
      "Content-Disposition: form-data; name=\"imagefile\"; filename=\"" + fileId + "\"\r\n" +
      "Content-Transfer-Encoding: base64\r\n" +
      "Content-Type: image/png\r\n\r\n" + content + "\r\n" +
      "--" + BOUNDARY + "--\r\n";
  String response = SendPostData(message);

  System.out.println("Chunk " + fileId + "_" + number + " " + response);
  return response;
}

/**
* Отправляет на сервер POST-запрос
* @param message Текст запроса
* @return Ответ сервера
*/
private static String SendPostData(String message)
{
  HttpConnection conn = null;
  InputStream is = null;
  OutputStream os = null;
  String response = null;
  try
  {          
    conn = (HttpConnection) Connector.open(HOST);

    // Задаем параметры запроса
    conn.setRequestMethod(HttpConnection.POST);
    conn.setRequestProperty("Connection", "Keep-Alive");
    conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);

    byte[] messageBody = message.getBytes();          
    conn.setRequestProperty("Content-Length", Integer.toString(messageBody.length));

    // Посылаем запрос
    os = conn.openOutputStream();
    os.write(messageBody);

    int rc = conn.getResponseCode();          

    if (rc != HttpConnection.HTTP_OK)
    {
      throw new IOException("HTTP responce code: " + rc);
    }

    // Получаем ответ
    is = conn.openInputStream();
    int ch;
    StringBuffer data = new StringBuffer();
    while ((ch = is.read()) != -1)          
    {
      data.append((char) ch);
    }
    response = data.toString();
  }
  catch (Exception e)
  {
    response = "Error while connecting:\n\n" + e.getMessage();
  }
  finally
  {
    try
    {
      if (is != null)
      {
        is.close();
      }              
      if (os != null)
      {
        os.close();
      }
      if (conn != null)
      {
        conn.close();
      }
    }
    catch (Exception e)
    {
    }
  }
  return response;
}

/**
* Проверка, успешно ли произошел Upload на сервер
* @param response Ответ сервера
* @return
*/
private boolean IsUploadOk(String response)
{
  return response.equals(UPLOAD_OK_MSG);
}

Сервер

Сервер, как и раньше, написан на .NET. Есть хэндлер Handler.ashx, и в нем написано следующее:
namespace MobileServer
{
  [WebService(Namespace = "http://tempuri.org/")]
  [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
  public class Handler : IHttpHandler
  {
    private const string UPLOAD_PATH = "...";
    
    /// <summary>
    /// Получение пути ко временному файлу с фрагментом картинки
    /// </summary>
    private static string GetChunkFileName(string fileName, int number)
    {
    return String.Format("{0}\{1}_{2}", UPLOAD_PATH, f.FileName, number);
    }

    /// <summary>
    /// Получение пути к файлу с картинкой
    /// </summary>
    private static string GetFileName(string fileName)
    {
    return String.Format("{0}\{1}.png", UPLOAD_PATH, f.FileName);
    }
    
    /// <summary>
    /// Сохранение фрагмента картинки в файл
    /// </summary>
    /// <param name="number">Номер фрагмента
    /// <param name="f">Файл, полученный по HTTP
    private static void SaveChunkToFile(int number, HttpPostedFile f)
    {
      byte[] buf = new byte[f.ContentLength];
      f.InputStream.Read(buf, 0, f.ContentLength);
      String content = String.Empty;

      for (int i = 0; i < buf.Length; ++i)
      {
        if (buf[i] == 0)
          break;

        content += ((char)buf[i]).ToString();
      }

      byte[] rez = Convert.FromBase64String(content);

      string fileName = GetChunkFileName(f.FileName, number);

      FileStream fs = new FileStream(fileName, FileMode.CreateNew);
      BinaryWriter bw = new BinaryWriter(fs);
      bw.Write(rez, 0, rez.Length);
      bw.Flush();
      bw.Close();
      fs.Close();
    }
    
    /// <summary>
    /// Проверка, все ли фрагменты данной картинки залились
    /// </summary>
    /// <param name="fileName">Имя файла с картинкой
    /// <param name="count">Общее количество фрагментов
    private static bool AreAllChunksUploaded(string fileName, int count)
    {
      for (int i = 1; i <= count; ++i)
      {
        if (!File.Exists(GetChunkFileName(fileName, i)))
        {
          return false;
        }
      }
      return true;
    }

    /// <summary>
    /// Объединение всех фрагментов файла, и сохранение в отдельный файл
    /// </summary>
    /// <param name="fileName">Название файла
    private static void MergeChunks(string fileName)
    {
      for (int i = 1; i <= imageInfo.ChunksCount; ++i)
      {
        FileStream fs = new FileStream(GetChunkFileName(fileName, i), FileMode.Open);
        BinaryReader br = new BinaryReader(fs);
        byte[] buf = br.ReadBytes(2000);
        br.Close();
        fs.Close();

        fs = new FileStream(GetFileName(fileName), FileMode.Append);
        BinaryWriter bw = new BinaryWriter(fs);
        bw.Write(buf);
        bw.Close();
        fs.Close();
      }
    }

    #region IHttpHandler members
    public void ProcessRequest(HttpContext context)
    {
      try
      {
        int number = Convert.ToInt32(context.Request["number"]);
        int count = Convert.ToInt32(context.Request["count"]);
        string sessionId = context.Request["sessionId"];

        HttpPostedFile f = (HttpPostedFile)context.Request.Files[0];

        SaveChunkToFile(number, f);

        if (AreAllChunksUploaded(f.FileName, count))
        {
          MergeChunks(f.FileName);
          //TODO: код для удаления временных файлов
        }
        context.Response.Write("Upload OK");
      }
      catch (Exception e)
      {
        context.Response.Write("Upload failed:\n" + e.Message);
      }
    }

    public bool IsReusable
    {
      get
      {
        return false;
      }
    }
    #endregion
  }
}
В общем, как-то так.