Visual Studio LightSwitch-Uvoz podataka iz CSV za desktop aplikacije

Pored izvoza podataka u Excel i CSV format, ponekad nam je potreban obrnut proces, da iz fajla učitamo podatke u određeni entitet/bazu podataka u okviru LightSwitch aplikacije.

LightSwitch razvojni tim je izdao Extenziju Excel Importer for Visual Studio LightSwitch kojom se može izvršiti uvoz podataka iz Excel fajla u određenu kolekciju na ekranu pri čemu je moguće izvršiti uparivanje kolona iz entieta sa kolonama iz Excel fajla.

U ovom postu ću prikazati uvoz podataka iz CSV fajla za Desktop aplikacije. Web aplikacije se u odnosu na desktop Silverlight aplikacije razlikuju po tome što imaju različit način pristupa resursima računara, tačnije različite privilegije te je stoga potrebno primjeniti različite metode pristupa da bi primjer uopšte funkcionisao.

Cjelokupan kod za ovaj primjer (Solution) možete pronaći na SkyDrive u folderu ImportData-CSV.

Import podataka iz CSV fajla – desktop aplikacija

  • Izvor: CSV fajl
  • Odredište: entitet bez spoljnih ključeva

U okviru klase (ekrana kreiranog na osnovu šablona Editable Grid Screen) ImportEmployeesDesktop kreiraću naredne funkcije za uvoz podataka. Uvoz podataka se vrši na osnovu dugmeda Import from Excel:

BeforeSavingGrid1

Programski kod:

partial void ImportExcel_Execute()
{
  ImportCSV(this.DataWorkspace.ApplicationData.Employees); //EntitySet that we want to import data from Excel
}

 

private void ImportCSV(IEntitySet entity)
{
  System.IO.FileInfo file = null;

  //Open OpenFileDialog to save file on file system
  // OpenFileDialog must be opened on the UI (Main) thread
  Microsoft.LightSwitch.Threading.Dispatchers.Main.BeginInvoke(() =>
  {
    OpenFileDialog dialog = new OpenFileDialog();
    dialog.Filter = "CSV (*.csv)|*.csv";

    if (dialog.ShowDialog() == true)
    {
      file = dialog.File;

      if (file != null)
      {
        ImportCSVData(file, entity);
      }
    }
  });
}

 

private void ImportCSVData(FileInfo file, IEntitySet entity)
{
  List parsedRow = new List();

  using (StreamReader streamReader = file.OpenText())
  {
    string line;
    string[] row;

    while ((line = streamReader.ReadLine()) != null)
    {
      row = line.Split(',');
      parsedRow.Add(row);
    }
    AddData(parsedRow, entity); //Import data

    this.Details.Dispatcher.BeginInvoke(() =>
    {
      this.DataWorkspace.ApplicationData.SaveChanges(); //Save data in logical thread, not in the Main thread.
    });
  }
}

 

private void AddData(List parsedRow, IEntitySet entity)
{
  int count = 0;
  int i = 0;
  IEntityProperty currentProperty; //property from the entity that we save data
  object newValue;
  string[] headers = null;

  foreach (string[] row in parsedRow)
  {
    if (i==0) //first row contain header
    {
      headers = row.Select(header => header.Trim()).ToArray();
      count = headers.Count(); //number of columns from header of the file
    }
    else
    {//non header rows
      if (count == row.Count()) //each row need to have the same number of data element as in header
      {
        IEntityObject newEntity = entity.AddNew(); //new row in entity
        //for each property from header row find coresponding property in entity and set value to it from the file
        for (int j = 0; j < count; j++)
        {
        try
        {
          //Current property from header row
          currentProperty = newEntity.Details.Properties[headers[j]];
        }
        catch (ArgumentException)
        {
          throw new InvalidOperationException(String.Format("A property named {0} does not exist on the entity named {1}.", headers[j], newEntity.Details.Name));
        }
        try
        {
          //Value for current property of the same type as entity property
          newValue = Convert.ChangeType(row[j], currentProperty.PropertyType, null);

          //set new value to property
          currentProperty.Value = newValue;
        }
        catch (System.FormatException)
        {
          throw new InvalidOperationException(String.Format("Line has an invalid value for property {0}. Aborting the import.\nData: {1}", headers[j], String.Join(",",row)));
        }
      }
    }
    else
    {//row doesn't have the same number of data element as header
    throw new InvalidOperationException(String.Format("Line not imported.  Invalid number of elements. Header has {0} columns and line number {1} has {2} columns",count, i,row.Count()));
    }
  }
  i++; //increment counter for the next row
  }
}

Korišćene su četiri metode:

  • ExportToCSV_Execute (metoda kojom se klikom na dugme inicira izvoz podataka)
  • ImportCSV (pokretanje dijaloga za izbor fajla koji želimo da učitamo)
  • ImportCSVData (čitanje fajla red po red)
  • AddData (dodavanja podataka u entitet)

Fajl koji ćemo učitati je EmployeesCSV.csv:

EmployeeCSV

Par napomena:

  • this.DataWorkspace.ApplicationData.SaveChanges() – snimanje podataka je potrebno postaviti u logički thread inače bi dobili grešku da pokušavamo odraditi funkionalnost u pogrešnom thread-u (Main thread)
  • Snimanje podataka se vrši samo u okviru jednog entiteta. Ukoliko bi trebali da snimamo istovremeno podatke u dva povezana entieta preko relacije onda mi trebali na osnovu podatka da pronađemo vrijednost na osnovu veze iz detail eniteta i da to iskoristimo za snimanje podatka. Vidjeti sekciju na kraju teksta pod naslovom: Uvoz podataka kada postoji veza između entiteta.

Umjesto petlje po kojoj se na osnovu naziva polja iz prvog reda (header) pronalazi odgovarajući property iz entiteta (metoda AddData) mogli smo pojednostaviti stvar i iskoristiti kod za kreiranje novog reda u entitetu i dodjelu vrijednosti na osnovu indeksa iz fajla (row – red koji sadrži niz elemenata pri čemu je svaki od elemenata jedna kolona):

Employee emp = this.DataWorkspace.ApplicationData.Employees.AddNew();
emp.FirstName = row[0];
emp.LastName = row[1];
emp.Email = row[2];
emp.PhoneNumber = row[3];
emp.Address = row[4];

Nedostatak: moramo voditi računa o redoslijedu podataka u fajlu i indeksu u programskom kodu što je nepraktično.

Ukoliko ne bi koristili snimanje podataka putem koda:

this.DataWorkspace.ApplicationData.SaveChanges();

onda bi imali mogućnost da prije snimanja podataka putem dugmeda Save ispravimo podatke i tek onda snimimo podatke (zvijezda pored prve kolone znači da podaci još nisu snimljeni u bazi nego ih je moguće promijeniti pa tek onda snimiti):

BeforeSavingGrid

Otvaranje dijaloga sa OpenFileDialog će raditi samo u Desktop aplikacijama jer se LightSwitch Desktop aplikacija (Silverlight Out of the Browser) vrti u okruženju koje omogućava pristup resurima računara. Ovaj primjer ne funkcioniše za Web aplikacije zbog Silverlight restrikcije pristupa file sistemu računara. Naime, Ukoliko bi ovu aplikaciju pokrenuli u Browser-u dobili bi poruku: Dialogs must be user-initiated zbog problema sa iniciranje otvaranja dijaloga za izbor fajla (OpenFileDialog). Razlog za ovo je što dijalog za otvaranje fajla u Silverlight-u mora biti inicirano aktivnošću korisnika kao što je na primjer event handler.

E sad, pitanje je zašto ovo ne funkcioniše u LightSwitch okruženju? Razlog tome je što je arhitektura LightSwitch aplikacije takva da je logika koja stoji iza dugmeta asinhrona (kako određena aktivnost koja dugo traje ne bi zaustavila izvršenje programa) te se kod koji se izvršava u logici dugmeta ne smatra iniciranim od strane korisnika (“user-initiated”). Da bi se prevazišao ovaj problem jedan od načina je da se kreira Custom Silverlight dialog čija će funkcija biti otvaranje OpenFileDialog ili SaveFileDialog.

Uvoz podataka kada postoji veza između entiteta

  • Izvor: CSV fajl
  • Odredište: entitet sa spoljnim ključem

     

Prethodni primjer je pokazao način uvoza podataka kada je u pitanju entitet u koji se vrši uvoz podataka a koji nema veze ka drugim entitetima tj. enitet bez spoljnih ključeva (IEntityStorageProperty). Prikazaću šta je potrebno odraditi za slučaj kada imamo spoljne ključeve uz malo pojednostavljen kod. Kreiraću ekran sa nazivom EditableStudentsGrid u koji ću postaviti logiku za uvoz podataka. Entiteti su:

  • Student (Id, Name, Surname)
  • City (Id, Name)

Student-City

Fajl koji je potrebno učitati u entitet Student je:

StudentCSV

Dakle, u situaciji kada imamo i spoljni ključ na tabelu City, nije dovoljno da postavimo vrijednost sledećim programskim kodom:

Student.City  = “Paris”;

jer tip polja City u entitetu Student nije tipa String neko je tipa City. Cjelokupan kod možete pogledati u priloženom Solution-u ali glavni dio koda koji radi ovu funkcionalnost je u metodu AddData:

Student student = this.DataWorkspace.ApplicationData.Students.AddNew();
student.Name = row[0];
student.Surname = row[1];

City city = this.DataWorkspace.ApplicationData.Cities.Where(a => a.Name == row[2]).SingleOrDefault();
if (null != city)
{
  //City exist
  student.City = city;
}
else
{
  //City don't exist - Don't do anything
  //User can add City and choose it from AutoCompleteBox in Editable Student Grid
}

Dakle, prvo je potrebno pronaći u entitetu City pojavu sloga čiji je naziv iz kolone koja odgovara nazivu mjesta u CSV fajlu StudentCSV.csv i potom postaviti vrijednost u enitetu Student.City na pronađenu vrijednost city. Krajnji rezultat je sledeći:

BeforeSavingStudentGrid

Kako za studenta u trećem redu nismo uspjeli pronaći naziv u entiteu City pod nazivom “Toronto” ostavili smo prazno polje kako bi mogli ručno odraditi unos novog grada te potom postaviti polje City za studenta na novounesenu vrijednost.

About Spaso Lazarevic

Spaso Lazarevic is Senior Software Developer working with Microsoft technologies. Leader of .NET User Group Bijeljina, speaker at Microsoft events, writter and blogger. Microsoft MVP for Visual C#.
This entry was posted in Programming and tagged , , , . Bookmark the permalink.

4 Responses to Visual Studio LightSwitch-Uvoz podataka iz CSV za desktop aplikacije

  1. asimze says:

    Šta čovjek da ti kaže??? Hvala! Vrlo lijepo i čitljivo. Nemoj mikada stediti na komentarima u kodu. Citaoci ipak nemaju znanja kao ti!!!

  2. Cause you ask for it🙂
    Hvala Asime. Primjetićeš da ću programski kod uvijek pisati na engleskom ali ću se truditi da još više komentarišem kod (što mi je inače običaj i kada ne pišem za blog).
    Ako je nešto nejasno, samo pitajte.
    Naravno, i dalje primam sugestije za naredne teme na ovom blogu.
    Veliki pozdrav

  3. Kivito says:

    bok Spaso! odlican clanak! htio sam te pitati, posto si spomenuo web aplikaciju, napravio sam za nju import iz csv-a ali me muci sto ne mogu importirati dijakriticke znakove poput č ž š.. dok sa desktop aplikacijom ide bez problema, pa me zanima ima li kakvo rjesenje za to? (mozda je neka sitnica u pitanju, a bilo bi idealno ne forsirati korisnika da ima instaliran excel i desktop aplikaciju)..
    hvala i pozdrav!

  4. Pozdrav Kivito i hvala za pohvale.
    Ne znam koju si metodu koristio za otvaranje fajla. Ja sam u ovom postu koristio OpenText() koji kreira StreamReader UTF8 encoding pri čemu se koristi klasa FileInfo.
    Analizirajući ostale klase za otvaranje koje sadrže Encoding kao parametar, mogao bi da probaš metode klase File kao što je metoda:
    public static string ReadAllText(string path, Encoding encoding);
    Ukoliko želiš da vidiš sve metode klase, možeš u programskom kodu otkucati System.IO.File fajl; i potom nad File iz kontektstnog menija (desno dugme miša) odabrati Go To Definition (F12).
    Na ovaj način, koristeći Encoding, možeš da isforsiraš željeni Encoding prilikom otvaranja fajla i možda na taj način da dođeš do naših slova.
    Pozdrav

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s