This is the continuation of the part 2 of this series.
Once again, the repository for the full app can be accessed on Github.
The Notes page will be containing most of the operations of the whole application. The notes are displayed via html/css using plain javascript Dom manipulation, and some back-end code in the Web Api Part. The data stored in database is retrieved via Fetch Api, and displayed in the page.
Then the posting of notes accesses the Web API also using Fetch, so does the update and delete ones.
I strongly recommend reviewing the below recommended tutorials before checking this part.
For Ajax/DOM manipulation
1. https://mydev-journey.blogspot.com/2020/02/mybooklist-another-traversy-tutorial.html
2. https://mydev-journey.blogspot.com/2020/02/expense-tracker-from-traversy-media.html
3. https://mydev-journey.blogspot.com/2020/02/full-stack-mini-todo-app-with.html
For the Web api part:
1. the above three tutorials + https://docs.microsoft.com/en-us/aspnet/core/tutorials/web-api-javascript?view=aspnetcore-3.1
2. https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-web-api?view=aspnetcore-3.1&tabs=visual-studio
So here I will write the code for the Web Apis created using EF CRUD (scaffolded automatically using the models presented in previous post, and then edited for additions).
The API controllers are in the Controller folders.
Users - it's completely Scaffolded from the Users Model Class, using the context, no additions written manually.
Notes:
contains the CRUD operations to create, delete, edit notes.
I recognize, maybe more checks and verification could have been done in the action methods, that could be your homework to do.
Notes Controller
namespace SmartNotes.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class NotesController : ControllerBase
{
private readonly SmartNotesDBContext _context;
public NotesController(SmartNotesDBContext context)
{
_context = context;
}
// GET: api/Notes
[HttpGet]
public async Task<ActionResult<IEnumerable<Notes>>> GetNotes()
{
return await _context.Notes.ToListAsync();
}
// GET: api/Notes/5
[HttpGet("{id}")]
public async Task<ActionResult<Notes>> GetNotes(int id)
{
var notes = await _context.Notes.FindAsync(id);
if (notes == null)
{
return NotFound();
}
return notes;
}
// this is a very important Get action method- retrieving list of notes by user, order and searchstring
[HttpGet("notesbyuser/{userid}/{order}/{searchstring}")]
public async Task<ActionResult<List<Notes>>> GetNotesByUser(int userid, string order="Desc", string searchstring="")
{
var notes = new List<Notes>();
if (searchstring == "(empty)") searchstring = "";
searchstring = searchstring.ToLower();
if (order=="Desc")
{
notes = await _context.Notes.Where(x => x.Userid == userid).OrderBy(x => x.Pinned).ThenByDescending(x=>x.Createdat).ToListAsync();
}
else
{
notes = await _context.Notes.Where(x => x.Userid == userid).OrderBy(x => x.Pinned).ThenBy(x => x.Createdat).ToListAsync();
}
if (notes == null)
{
return NotFound();
}
return notes.Where(x=> x.Title.ToLower().Contains(searchstring) || x.NoteText.ToLower().Contains(searchstring)).ToList();
}
// PUT: api/Notes/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://aka.ms/RazorPagesCRUD.
[HttpPut("{id}")]
public async Task<IActionResult> PutNotes(int id, Notes notes)
{
if (id != notes.Id)
{
return BadRequest();
}
_context.Entry(notes).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!NotesExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/Notes
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://aka.ms/RazorPagesCRUD.
[HttpPost]
public async Task<ActionResult<Notes>> PostNotes(Notes notes)
{
_context.Notes.Add(notes);
await _context.SaveChangesAsync();
return CreatedAtAction("PostNotes", new { id = notes.Id }, notes);
}
// action to pin note
[HttpPost("pinnote/{noteid}")]
public async Task<ActionResult<Notes>> PinNote(int noteid)
{
var myNote = await _context.Notes.FindAsync(noteid);
if (myNote!=null)
{
myNote.Pinned = !myNote.Pinned;
_context.Notes.Update(myNote);
await _context.SaveChangesAsync();
return Ok();
}
return Ok();
}
// action to change the color of a note
[HttpPut("changecolor/{noteid}")]
public async Task<ActionResult<Notes>> ChangeColor(int noteid, Notes notes)
{
var myNote = await _context.Notes.FindAsync(noteid);
if (myNote != null)
{
myNote.Color = notes.Color;
_context.Notes.Update(myNote);
await _context.SaveChangesAsync();
return Ok();
}
return Ok();
}
// a put action to update a note, by id
[HttpPut("updatenote/{noteid}")]
public async Task<ActionResult<Notes>> UpdateNote(int noteid, Notes notes)
{
var myNote = await _context.Notes.FindAsync(noteid);
if (myNote != null)
{
myNote.Title = notes.Title;
myNote.NoteText = notes.NoteText;
_context.Notes.Update(myNote);
await _context.SaveChangesAsync();
return Ok();
}
return Ok();
}
// DELETE: api/Notes/5
// action to delete the note and respective images, by note id
[HttpDelete("{id}")]
public async Task<ActionResult<Notes>> DeleteNotes(int id)
{
var images = await _context.Images.Where(x => x.Noteid == id).ToListAsync();
foreach (var img in images)
{
var filepath =
new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "Uploads")).Root + $@"\{img.Image}";
System.IO.File.Delete(filepath);
}
if (images!=null) _context.Images.RemoveRange(images);
var notes = await _context.Notes.FindAsync(id);
if (notes == null)
{
return NotFound();
}
_context.Notes.Remove(notes);
await _context.SaveChangesAsync();
return Ok();
}
private bool NotesExists(int id)
{
return _context.Notes.Any(e => e.Id == id);
}
}
}
Image Controller - will deal with uploading/deleting images
namespace SmartNotes.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ImagesController : ControllerBase
{
private readonly SmartNotesDBContext _context;
public ImagesController(SmartNotesDBContext context)
{
_context = context;
}
// GET: api/Images
[HttpGet]
public async Task<ActionResult<IEnumerable<Images>>> GetImages()
{
return await _context.Images.ToListAsync();
}
// GET: api/Images/5
[HttpGet("{id}")]
public async Task<ActionResult<Images>> GetImages(int id)
{
var images = await _context.Images.FindAsync(id);
if (images == null)
{
return NotFound();
}
return images;
}
// retrieves all images by note id (to display them in the note)
[HttpGet("imagesbynote/{noteid}")]
public async Task<ActionResult<List<Images>>> GetImagesByNote(int noteid)
{
var images = await _context.Images.Where(x=> x.Noteid ==noteid).ToListAsync();
if (images == null)
{
return NotFound();
}
return images;
}
// retrieves all images by user id (to display them in the note page)
[HttpGet("imagesbyuser/{userid}")]
public async Task<ActionResult<List<Images>>> GetImagesByUser(int userid)
{
var images = await _context.Images.ToListAsync();
if (images == null)
{
return NotFound();
}
return images;
}
// PUT: api/Images/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://aka.ms/RazorPagesCRUD.
[HttpPut("{id}")]
public async Task<IActionResult> PutImages(int id, Images images)
{
if (id != images.Id)
{
return BadRequest();
}
_context.Entry(images).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ImagesExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/Images
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://aka.ms/RazorPagesCRUD.
//[HttpPost]
//public async Task<ActionResult<Images>> PostImages(Images images)
//{
// _context.Images.Add(images);
// await _context.SaveChangesAsync();
// return CreatedAtAction("GetImages", new { id = images.Id }, images);
//}
// uploading one image, and link it to note having noteid
[HttpPost("uploadimage/{noteid}")]
public async Task<ActionResult<Images>> PostUpload( int noteid, IFormFile image)
{
if (image != null && noteid!=0 && image.Length > 0 && image.Length < 500000)
{
try
{
var fileName = Path.GetFileName(image.FileName);
//Assigning Unique Filename (Guid)
var myUniqueFileName = Convert.ToString(Guid.NewGuid());
//Getting file Extension
var fileExtension = Path.GetExtension(fileName);
// concatenating FileName + FileExtension
var newFileName = String.Concat(myUniqueFileName, fileExtension);
// Combines two strings into a path.
var filepath =
new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "Uploads")).Root + $@"\{newFileName}";
using (FileStream fs = System.IO.File.Create(filepath))
{
image.CopyTo(fs);
fs.Flush();
}
var newImage = new Images();
newImage.Image = newFileName;
newImage.Noteid = noteid;
_context.Images.Add(newImage);
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
return StatusCode(500);
}
//Getting FileName
var myImageList = await _context.Images.Where(x => x.Noteid == noteid).ToListAsync();
return Ok(myImageList);
}
return NoContent();
}
// DELETE: api/Images/5
[HttpDelete("{id}")]
public async Task<ActionResult<Images>> DeleteImages(int id)
{
var images = await _context.Images.FindAsync(id);
if (images == null)
{
return NotFound();
}
_context.Images.Remove(images);
await _context.SaveChangesAsync();
return images;
}
// delete images by note, when removing a note
[HttpDelete("deleteimagesbynote/{noteid}")]
public async Task<ActionResult<Images>> DeleteImagesByNote(int noteid)
{
var images = await _context.Images.Where(x=> x.Noteid == noteid).ToListAsync();
if (images == null)
{
return NotFound();
}
foreach (var img in images)
{
deleteImage(img.Image);
_context.Images.Remove(img);
}
await _context.SaveChangesAsync();
return Ok();
}
private void deleteImage(string imagefile)
{
var filepath =
new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "Uploads")).Root + $@"\{imagefile}";
System.IO.File.Delete(filepath);
}
private bool ImagesExists(int id)
{
return _context.Images.Any(e => e.Id == id);
}
}
}
Thursday, February 27, 2020
Wednesday, February 26, 2020
Full Stack Asp.Net Core App (Bootcamp Project) - Part 2 - The Database and (Razor) Pages
This is the continuation of the material in this link: https://mydev-journey.blogspot.com/2020/02/full-stack-aspnet-core-app-bootcamp.html
Database and entities/models
The main entities of the database will be:
- users: will store the username and their password(not encrypted! bad practice), and their id
- notes: title, content, userid, color
- images: noteid, file name.
Let's have a look at the database script, which defines the relationships.
Using EF Core, the database is scaffolded into a Model folder.
The model classes will look in the following way(as scaffolded by EF Core):
public partial class Users
{
public Users()
{
Notes = new HashSet<Notes>();
}
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public virtual ICollection<Notes> Notes { get; set; }
// I will use this to store the confirmed password, not save in the DB
[NotMapped]
public string Password2 { get; set; }
}
public partial class Notes
{
public Notes()
{
Images = new HashSet<Images>();
}
public int Id { get; set; }
public int Userid { get; set; }
public string Title { get; set; }
public string NoteText { get; set; }
public DateTime Createdat { get; set; }
public bool Pinned { get; set; }
public string Color { get; set; }
public virtual Users User { get; set; }
public virtual ICollection<Images> Images { get; set; }
}
public partial class Images
{
public int Id { get; set; }
public int Noteid { get; set; }
public string Image { get; set; }
public virtual Notes Note { get; set; }
}
For an existing database, it can be scaffolded into a context and models using the following instructions.
The Github Repo of the project is here.
The structure of wwwroot folder:
css: will contain the manually written css files for each served page
images: will contain the images that belong to the html of the pages
js and lib: can be empty.
uploads: will contain the result of the uploads, images that will appear in each note.
The Pages
The pages are served by Asp.Net Core Razor pages, which is the basic project in Asp.Net Core. (version used here is 3.1, LTS). Each page will have its own css file, present in wwwroot css folder. Their respective html code will be in the cshtml of the Razor Page, only the Notes Page having lots of Javascript included also. I recommend very strongly that you review Razor Pages because they are serving the pages.
The CSS and Html
The Css was written manually (I will not go in detail here) together with the Html, according to a design template. It defines the structure of the files in the view part of the pages below.
Each html file will have its own css. The Layouts will be null in each of the below Razor Pages. You can see the html/css in the Github repo, although this was a part of the bootcamp, I will not go through them.
I will insert more comments here on the blog, than in the Github, to make it more understandable. I will not build the pages step by step, instead just show the comments and explanations regarding the code.
a. Index Page
The PageModel code: - nothing special, here.
The html, you can find on the Github Demo.
b. Signup Page
public class SignUpModel : PageModel
{
private readonly SmartNotesDBContext _context;
public SignUpModel(SmartNotesDBContext context)
{
_context = context;
}
// below property will contain the user that will be created, linked to the page using binding
[BindProperty]
public Users newUser { get; set; }
// below property will be used to display an error message, linked to the page using binding
[BindProperty]
public string errorMessage { get; set; }
// this will display the error message if the user signup did not work well
public void OnGet(string err)
{
errorMessage = err;
}
// basic email validation function
bool IsValidEmail(string email)
{
try
{
var addr = new System.Net.Mail.MailAddress(email);
return addr.Address == email;
}
catch
{
return false;
}
}
// checks if any other user has the same email, which have to be unique in the database.
bool IsExistingEmail(string email)
{
return _context.Users.Any(x => x.Email == email);
}
// posting the form on the SignUp page, and collecting /saving the user data in the database.
public async Task<IActionResult> OnPost()
{
newUser.Email = newUser.Email.Trim();
if (!IsValidEmail(newUser.Email))
{
errorMessage = "Use a valid email address!";
return RedirectToPage("./SignUp", new { err = errorMessage});
}
if (IsExistingEmail(newUser.Email))
{
errorMessage = "This Email Address has already been used!";
return RedirectToPage("./SignUp", new { err = errorMessage });
}
if (newUser.Password!=newUser.Password2)
{
errorMessage = "The passwords do not match!";
return RedirectToPage("./SignUp", new { err = errorMessage });
}
try
{
await _context.Users.AddAsync(newUser);
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
// error message is generated and page redirected
errorMessage = "Error with signup. Please try again later.";
return RedirectToPage("./SignUp", new { err = errorMessage });
}
// when signup was sucessful, user will be redirected to login.
return RedirectToPage("./Login");
}
}
c. Login Page
public class LoginModel : PageModel
{
private readonly SmartNotesDBContext _context;
public LoginModel(SmartNotesDBContext context)
{
_context = context;
}
// the user who tries to log in
[BindProperty]
public Users LoginUser { get; set; }
// the error message which will be shown in the html in case of unsuccesful attempt
[BindProperty]
public string errorMessage { get; set; }
public void OnGet(string err)
{
errorMessage = err;
}
// login, posting the form
public async Task<IActionResult> OnPost()
{
// try to find the user in the table having email and password provided
var myUser = new Users();
try
{
myUser = await _context.Users.FirstAsync(x => x.Email == LoginUser.Email && x.Password == LoginUser.Password);
}
catch (Exception ex)
{
errorMessage = "Invalid User/Password";
// if no user found, error message shown on page in the form.
return RedirectToPage("./Login", new { err = errorMessage });
}
// save the user in the session
SessionHelper.SetObjectAsJson(HttpContext.Session, "loginuser", myUser);
// if user found, it's logged in and redirected to notes.
return RedirectToPage("./Notes");
}
}
d. Notes Page
public class NotesModel : PageModel
{
// this user id will be used in the html/javascript of the page
public int LoginUserID { get; set; }
// this email address will be used in the html/javascript of the page
public string LoginUserEmail { get; set; }
// will take the session value of the logged in user and serve the page. if no user is logged in, will redirect to Login page.
public async Task<IActionResult> OnGet()
{
//check if the user arrived here using the login page, then having the loginuser properly setup
var loginuser = SessionHelper.GetObjectFromJson<Users>(HttpContext.Session, "loginuser");
// if no user logged in using login page, redirect to login
if (loginuser == null)
{
return RedirectToPage("./Login");
}
//just pickup the user id and email to show it on the page (and use them in the js code), see html code
LoginUserID = loginuser.Id;
LoginUserEmail = loginuser.Email;
return Page();
}
}
e. Sign out
public class LogoutModel : PageModel
{
public IActionResult OnGet()
{
// logoout page deleting the logged in user and redirecting to main page.
SessionHelper.SetObjectAsJson(HttpContext.Session, "loginuser", null);
return RedirectToPage("./Index");
}
}
f. Error page
this is mainly a notfound page, written in html/css. Nothing special on the PageModel code.
The Web API back-end of the application will dealt with the CRUD operations, and this will be dealt in the next post.
Database and entities/models
The main entities of the database will be:
- users: will store the username and their password(not encrypted! bad practice), and their id
- notes: title, content, userid, color
- images: noteid, file name.
Let's have a look at the database script, which defines the relationships.
Using EF Core, the database is scaffolded into a Model folder.
The model classes will look in the following way(as scaffolded by EF Core):
public partial class Users
{
public Users()
{
Notes = new HashSet<Notes>();
}
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public virtual ICollection<Notes> Notes { get; set; }
// I will use this to store the confirmed password, not save in the DB
[NotMapped]
public string Password2 { get; set; }
}
public partial class Notes
{
public Notes()
{
Images = new HashSet<Images>();
}
public int Id { get; set; }
public int Userid { get; set; }
public string Title { get; set; }
public string NoteText { get; set; }
public DateTime Createdat { get; set; }
public bool Pinned { get; set; }
public string Color { get; set; }
public virtual Users User { get; set; }
public virtual ICollection<Images> Images { get; set; }
}
public partial class Images
{
public int Id { get; set; }
public int Noteid { get; set; }
public string Image { get; set; }
public virtual Notes Note { get; set; }
}
For an existing database, it can be scaffolded into a context and models using the following instructions.
The Github Repo of the project is here.
The structure of wwwroot folder:
css: will contain the manually written css files for each served page
images: will contain the images that belong to the html of the pages
js and lib: can be empty.
uploads: will contain the result of the uploads, images that will appear in each note.
The Pages
The pages are served by Asp.Net Core Razor pages, which is the basic project in Asp.Net Core. (version used here is 3.1, LTS). Each page will have its own css file, present in wwwroot css folder. Their respective html code will be in the cshtml of the Razor Page, only the Notes Page having lots of Javascript included also. I recommend very strongly that you review Razor Pages because they are serving the pages.
The CSS and Html
The Css was written manually (I will not go in detail here) together with the Html, according to a design template. It defines the structure of the files in the view part of the pages below.
Each html file will have its own css. The Layouts will be null in each of the below Razor Pages. You can see the html/css in the Github repo, although this was a part of the bootcamp, I will not go through them.
I will insert more comments here on the blog, than in the Github, to make it more understandable. I will not build the pages step by step, instead just show the comments and explanations regarding the code.
a. Index Page
The PageModel code: - nothing special, here.
The html, you can find on the Github Demo.
b. Signup Page
public class SignUpModel : PageModel
{
private readonly SmartNotesDBContext _context;
public SignUpModel(SmartNotesDBContext context)
{
_context = context;
}
// below property will contain the user that will be created, linked to the page using binding
[BindProperty]
public Users newUser { get; set; }
// below property will be used to display an error message, linked to the page using binding
[BindProperty]
public string errorMessage { get; set; }
// this will display the error message if the user signup did not work well
public void OnGet(string err)
{
errorMessage = err;
}
// basic email validation function
bool IsValidEmail(string email)
{
try
{
var addr = new System.Net.Mail.MailAddress(email);
return addr.Address == email;
}
catch
{
return false;
}
}
// checks if any other user has the same email, which have to be unique in the database.
bool IsExistingEmail(string email)
{
return _context.Users.Any(x => x.Email == email);
}
// posting the form on the SignUp page, and collecting /saving the user data in the database.
public async Task<IActionResult> OnPost()
{
newUser.Email = newUser.Email.Trim();
if (!IsValidEmail(newUser.Email))
{
errorMessage = "Use a valid email address!";
return RedirectToPage("./SignUp", new { err = errorMessage});
}
if (IsExistingEmail(newUser.Email))
{
errorMessage = "This Email Address has already been used!";
return RedirectToPage("./SignUp", new { err = errorMessage });
}
if (newUser.Password!=newUser.Password2)
{
errorMessage = "The passwords do not match!";
return RedirectToPage("./SignUp", new { err = errorMessage });
}
try
{
await _context.Users.AddAsync(newUser);
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
// error message is generated and page redirected
errorMessage = "Error with signup. Please try again later.";
return RedirectToPage("./SignUp", new { err = errorMessage });
}
// when signup was sucessful, user will be redirected to login.
return RedirectToPage("./Login");
}
}
c. Login Page
public class LoginModel : PageModel
{
private readonly SmartNotesDBContext _context;
public LoginModel(SmartNotesDBContext context)
{
_context = context;
}
// the user who tries to log in
[BindProperty]
public Users LoginUser { get; set; }
// the error message which will be shown in the html in case of unsuccesful attempt
[BindProperty]
public string errorMessage { get; set; }
public void OnGet(string err)
{
errorMessage = err;
}
// login, posting the form
public async Task<IActionResult> OnPost()
{
// try to find the user in the table having email and password provided
var myUser = new Users();
try
{
myUser = await _context.Users.FirstAsync(x => x.Email == LoginUser.Email && x.Password == LoginUser.Password);
}
catch (Exception ex)
{
errorMessage = "Invalid User/Password";
// if no user found, error message shown on page in the form.
return RedirectToPage("./Login", new { err = errorMessage });
}
// save the user in the session
SessionHelper.SetObjectAsJson(HttpContext.Session, "loginuser", myUser);
// if user found, it's logged in and redirected to notes.
return RedirectToPage("./Notes");
}
}
d. Notes Page
public class NotesModel : PageModel
{
// this user id will be used in the html/javascript of the page
public int LoginUserID { get; set; }
// this email address will be used in the html/javascript of the page
public string LoginUserEmail { get; set; }
// will take the session value of the logged in user and serve the page. if no user is logged in, will redirect to Login page.
public async Task<IActionResult> OnGet()
{
//check if the user arrived here using the login page, then having the loginuser properly setup
var loginuser = SessionHelper.GetObjectFromJson<Users>(HttpContext.Session, "loginuser");
// if no user logged in using login page, redirect to login
if (loginuser == null)
{
return RedirectToPage("./Login");
}
//just pickup the user id and email to show it on the page (and use them in the js code), see html code
LoginUserID = loginuser.Id;
LoginUserEmail = loginuser.Email;
return Page();
}
}
e. Sign out
public class LogoutModel : PageModel
{
public IActionResult OnGet()
{
// logoout page deleting the logged in user and redirecting to main page.
SessionHelper.SetObjectAsJson(HttpContext.Session, "loginuser", null);
return RedirectToPage("./Index");
}
}
f. Error page
this is mainly a notfound page, written in html/css. Nothing special on the PageModel code.
The Web API back-end of the application will dealt with the CRUD operations, and this will be dealt in the next post.
Monday, February 24, 2020
MyBookList - another Traversy tutorial transposed with Asp.Net Backend
It's a Vanilla JS tutorial, using only front-end and local storage in browser. The local storage is replaced by a Web API back-end from Asp.Net Core, with In Memory database. Original tutorial from Brad.
Because of using a back-end with and on front-end, the fetch API, his code suffered some small modification in this case.
The application is live under: https://booklist.zoltanhalasz.net/
The my code has the repo: https://github.com/zoltanhalasz/BookListTraversy
Because of using a back-end with and on front-end, the fetch API, his code suffered some small modification in this case.
The application is live under: https://booklist.zoltanhalasz.net/
The my code has the repo: https://github.com/zoltanhalasz/BookListTraversy
Sunday, February 23, 2020
Expense-Tracker (from Traversy Media) With Asp.Net Core Back-end
In order to continue my practice of front-end javascript, I copied Brad's repo for the front-end (plain JS version) to check and learn from it.
I transformed it to a full-stack app using Asp.Net Core Razor Pages, In-Memory Database and API Controller.
The application is online: https://expensetracker.zoltanhalasz.net/
My repo is: https://github.com/zoltanhalasz/ExpenseTracker
The main difference is that his version uses the local storage of the browser, whereas mine has back-end using in-Memory database.
I hope that some of you find this useful.
Saturday, February 22, 2020
Full Stack Asp.Net Core App (Bootcamp Project) - Part 1 - Introduction
In the last weeks I decided to review my Javascript front-end lessons from last year's bootcamp. It was a locally organized intensive course, with the aim of hiring those who finish it.
I worked hard to learn javascript and node.js on that course, graduated the bootcamp, but finally remained with my .net projects for my former employer instead of being hired by the organizer of the bootcamp.
For your info, I described the bootcamp in more detail in this post.
Just to review Vanilla JS once again, I decided to re-do the project, this time with Asp.Net Core Backend instead of Node.JS, just to practice my API skills in Asp.Net Also.
The aim of the project is, to do things manually, without the use of any front-end frameworks:
Full code of the app can be found: https://github.com/zoltanhalasz/SmartNotes.git
The SQL to create the database is: under the Github folder above, file: script.sql
I won't promise that I will lead you through the application step by step, because of its complexity and also it's a study project, nothing perfect in it, but can be a great learning tool for you. :)
What the application does not include:
- no special user management, identity, authorizations, no password hashing, only basic cookie-based user login/authentication
- no special protection for the Web APIs
- no Jquery or JS Framework on the frontend, only basic Vanilla JS, with Fetch for AJAX
- no dashboard or advanced features, statistics
- it is not a perfect application, from the formatting or design/engineering point of view. It is a sample to learn the above mentioned features.
Description of the project
- Manage notes/(small blog posts) of users - add notes: title, content, add color, add images;
Navigate between notes and images, edit existing notes, search and sort notes.
- Signup of Users - collect email, password and name from user
- Login Users - based on name and password
The application has just a couple of pages, in the following logical order:
Index/Home Page
Signup Page
The design here is also manual, integrated into Razor Pages (no layout). The user can register by filling in basic info.
Login Page
In order to write notes, users must log in, using this page. Very basic design, manually written.
Notes Page
This is the main page of the app, users who are logged in, can create and manage notes. Color can be changed, images can be added and the title/content can be edited for each note. Additionally, searching and ordering the notes is possible.
Error Page
I worked hard to learn javascript and node.js on that course, graduated the bootcamp, but finally remained with my .net projects for my former employer instead of being hired by the organizer of the bootcamp.
For your info, I described the bootcamp in more detail in this post.
Just to review Vanilla JS once again, I decided to re-do the project, this time with Asp.Net Core Backend instead of Node.JS, just to practice my API skills in Asp.Net Also.
The aim of the project is, to do things manually, without the use of any front-end frameworks:
- writing the pages in plain html, and all styling in css without bootstrap or other systems
- all interactions with user will be via plain Javascript
- the back-end Api-s are Asp.Net Core Web API
- the pages are served via Asp.Net Core Razor Pages
- database for back-end MS SQL with EF Core (database-first)
- basic EF Core and MS SQL with database first approach: https://www.entityframeworktutorial.net/efcore/create-model-for-existing-database-in-ef-core.aspx
- basic Razor Pages for Asp.Net Core 3.1 / https://mydev-journey.blogspot.com/2019/11/razor-pages-not-for-shaving.html
- Web API For Asp.Net Core
- upper-intermediate Javascript on DOM (as preparation pls check this one: https://mydev-journey.blogspot.com/2020/02/full-stack-mini-todo-app-with.html)
- intermediate Css and Html
Full code of the app can be found: https://github.com/zoltanhalasz/SmartNotes.git
The SQL to create the database is: under the Github folder above, file: script.sql
I won't promise that I will lead you through the application step by step, because of its complexity and also it's a study project, nothing perfect in it, but can be a great learning tool for you. :)
What the application does not include:
- no special user management, identity, authorizations, no password hashing, only basic cookie-based user login/authentication
- no special protection for the Web APIs
- no Jquery or JS Framework on the frontend, only basic Vanilla JS, with Fetch for AJAX
- no dashboard or advanced features, statistics
- it is not a perfect application, from the formatting or design/engineering point of view. It is a sample to learn the above mentioned features.
Description of the project
- Manage notes/(small blog posts) of users - add notes: title, content, add color, add images;
Navigate between notes and images, edit existing notes, search and sort notes.
- Signup of Users - collect email, password and name from user
- Login Users - based on name and password
The application has just a couple of pages, in the following logical order:
Index/Home Page
This is the landing page for the application. It's plain html with css written manually, integrated into Razor Pages Index.CsHtml
From this page, users can signup or login.
Signup Page
The design here is also manual, integrated into Razor Pages (no layout). The user can register by filling in basic info.
Login Page
In order to write notes, users must log in, using this page. Very basic design, manually written.
Notes Page
This is the main page of the app, users who are logged in, can create and manage notes. Color can be changed, images can be added and the title/content can be edited for each note. Additionally, searching and ordering the notes is possible.
Error Page
Labels:
asp.net core,
bootcamp,
css,
dom,
experience,
javascript,
Razor,
Tutorial
Tuesday, February 18, 2020
Full Stack Mini ToDo-App With Javascript, Ajax, API Controller and In-Memory Database (Asp.Net Core Razor Pages)
During the last couple of days, I decided to revisit my basic DOM Javascript skills, and among other things, decided to write some mini-projects to exercise.
The topics touched in this tutorial are:
1. Front-end Javascript for DOM manipulation
2. Fetch API
3. Web Api controller in Asp.Net Core
4. In-Memory database for EF Core
5. Razor pages project
Materials to follow:
1. Main inspiration of the tutorial was Ajax tutorials from Dennis Ivy (front-end is 90% from him) https://www.youtube.com/watch?v=hISSGMafzvU&t=1157s
2. Repo for the app is: https://github.com/zoltanhalasz/TodoApp
3. In-Memory database used here (check my materials with Razor pages or https://exceptionnotfound.net/ef-core-inmemory-asp-net-core-store-database/)
4. Web Api - generated from EF Core CRUD automatically in Visual Studio, from the model
5. Application is live under: https://todolist.zoltanhalasz.net/
Main steps of the app:
1. Create Razor Pages App, without authentication
2. Create Class for ToDO
public class ToDoModel
{
public int id { get; set; }
public string title { get; set; }
public bool completed { get; set; }
}
3. Based on the class, the context is created with a table and included in startup.cs. EntityFrameworkCore has to be installed as nuget package.
public class ToDoContext : DbContext
{
public ToDoContext(DbContextOptions<ToDoContext> options)
: base(options)
{
}
public DbSet<ToDoModel> ToDoTable { get; set; }
}
and in the ConfigureServices method/startup.cs
services.AddDbContext<ToDoContext>(options => options.UseInMemoryDatabase(databaseName: "ToDoDB"));
4. Add a Controller folder, then scaffold Web-api (CRUD with EF Core), can be done based on above class and Context.
5. Front-End, content of index.cshtml file:
Tuesday, February 11, 2020
AG-Grid Tutorial With Asp.Net Core Razor Pages - Part 3
Prerequisites:
1. Check previous tutorial, part 2
2. Check documentation for Ag-Grid, I will link it under the right point
3. App is online: https://ag-grid3.zoltanhalasz.net/
4. Repository for this tutorial: https://drive.google.com/open?id=1WAbKlYsg3lpbfwE-NfYOmHFFtYPOck_f
5. Intermediate Javascript and Asp.Net Core Razor Pages
As a continuation of my study for this grid system, I checked the following options:
1. Row selection, and removal of rows using Ajax
https://www.ag-grid.com/javascript-grid-selection/
The goal here is to select the rows and remove them from the list and the database table.
The selection is described in above link and implemented in the javascript part of the index page.
function deleteSelectedRows() {
var selectedNodes = gridOptions.api.getSelectedNodes()
var selectedData = selectedNodes.map(function (node) { return node.data })
if (selectedData) {
postData('./api/InvoiceModels', selectedData)
.then((data) => {
console.log("response", data); // JSON data parsed by `response.json()` call
});
}
In order to do this, I created an api controller, for the InvoiceModel class and placed it in the Controllers Folder. Then rewrote the post routing, which takes a list of invoicemodel as an input, and then deletes them from the table:
// POST: api/InvoiceModels
[HttpPost]
public async Task<ActionResult<List<InvoiceModel>>> PostInvoiceModel(List<InvoiceModel> invoiceModels)
{
if (invoiceModels==null) return NoContent();
foreach (var model in invoiceModels)
{
var myinvoiceModel = await _context.InvoiceTable.FindAsync(model.ID);
if (myinvoiceModel != null)
{
_context.InvoiceTable.Remove(myinvoiceModel);
}
}
await _context.SaveChangesAsync();
return await _context.InvoiceTable.ToListAsync();
}
There is a Fetch Command in the index page which communicates with this Api route :
async function postData(url = '', data = {}) {
console.log(JSON.stringify(data), url);
await fetch(url, {
method: 'POST', // *GET, POST, PUT, DELETE, etc.
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data) // body data type must match "Content-Type" header
}).then(function (response) {
console.log(response.json());
}).then(function (response) {
agGrid.simpleHttpRequest({ url: './Index?handler=ArrayData' }).then(function (data) {
gridOptions.api.setRowData(data);
});
}).catch(() => console.log("err"));
}
2. Filtering Options
please study: https://www.ag-grid.com/javascript-grid-filter-provided-simple/
a. with filter set to false the filter is invisible for that column
b. for numeric filters, we can use the below (actually this will be with a range)
{
headerName: "Amount", field: "Amount",
filter: 'agNumberColumnFilter', filterParams: {
filterOptions: ['inRange']
}
},
c. for the category, a text filter will be used:
{
headerName: "CostCategory", field: "CostCategory", checkboxSelection: true,
filter: 'agTextColumnFilter', filterParams: {
defaultOption: 'startsWith',
resetButton: true,
applyButton: true,
debounceMs: 200
}
},
3. Data import
a. I saved the import.xlsx file under wwwroot/uploads, having the following structure
(below data is tab delimited)
ID InvoiceNumber Amount CostCategory Period
1 1 500.00 Utilities 2019_11
2 2 121.69 Telephone 2019_12
3 3 342.61 Services 2019_11
4 4 733.21 Consultancy 2019_11
5 5 107.79 Raw materials 2019_10
6 6 161.44 Raw materials 2019_11
7 7 334.48 Raw materials 2019_11
8 8 504.63 Services 2019_11
9 8 869.44 Services 2019_11
10 9 401.57 Services 2019_11
b. I checked the upload documentation and took their example https://www.ag-grid.com/example-excel-import/
c. please see the implementation of point b, it is almost copy-paste, just adding my columns instead:
After pushing the import button, the grid is filled in with the values from import.xlsx placed in wwwroot/uploads.
result will be:
The contents of the import page:
@page
@model DataTables.ImportModel
@{
ViewData["Title"] = "Import";
Layout = "~/Pages/Shared/_Layout.cshtml";
}
<h1>Import from pre-uploaded excel file (import.xlsx)</h1>
<script src="https://unpkg.com/xlsx-style@0.8.13/dist/xlsx.full.min.js"></script>
<script src="~/js/ag-grid-community.min.js"></script>
<div class="container">
<div class="row">
<button onclick="importExcel()" class="btn-outline-info">Import Excel</button>
</div>
<div class="row">
<div id="myGrid" style="height: 500px;width:650px;" class="ag-theme-balham-dark"></div>
</div>
</div>
<script>
// XMLHttpRequest in promise format
function makeRequest(method, url, success, error) {
var httpRequest = new XMLHttpRequest();
httpRequest.open("GET", url, true);
httpRequest.responseType = "arraybuffer";
httpRequest.open(method, url);
httpRequest.onload = function () {
success(httpRequest.response);
};
httpRequest.onerror = function () {
error(httpRequest.response);
};
httpRequest.send();
}
// read the raw data and convert it to a XLSX workbook
function convertDataToWorkbook(data) {
/* convert data to binary string */
console.log(data);
var data = new Uint8Array(data);
var arr = new Array();
for (var i = 0; i !== data.length; ++i) {
arr[i] = String.fromCharCode(data[i]);
}
var bstr = arr.join("");
return XLSX.read(bstr, { type: "binary" });
}
// pull out the values we're after, converting it into an array of rowData
function populateGrid(workbook) {
// our data is in the first sheet
//console.log(workbook);
var firstSheetName = workbook.SheetNames[0];
var worksheet = workbook.Sheets[firstSheetName];
// we expect the following columns to be present
var columns = {
'A': 'ID',
'B': 'InvoiceNumber',
'C': 'Amount',
'D': 'CostCategory',
'E': 'Period'
};
var rowData = [];
// start at the 2nd row - the first row are the headers
var rowIndex = 2;
// iterate over the worksheet pulling out the columns we're expecting
while (worksheet['A' + rowIndex]) {
var row = {};
Object.keys(columns).forEach(function (column) {
row[columns[column]] = worksheet[column + rowIndex].w;
});
rowData.push(row);
console.log(row);
rowIndex++;
}
// finally, set the imported rowData into the grid
gridOptions.api.setRowData(rowData);
}
function importExcel() {
makeRequest('GET',
'/uploads/import.xlsx',
// success
function (data) {
var workbook = convertDataToWorkbook(data);
populateGrid(workbook);
},
// error
function (error) {
throw error;
}
);
}
// specify the columns
var columnDefs = [
{ headerName: "ID", field: "ID", width: 50 },
{ headerName: "InvoiceNumber", field: "InvoiceNumber", width: 80 },
{ headerName: "Amount", field: "Amount", width: 100 },
{ headerName: "CostCategory", field: "CostCategory" },
{ headerName: "Period", field: "Period" },
];
// no row data to begin with
var rowData = [];
// let the grid know which columns and what data to use
var gridOptions = {
columnDefs: columnDefs,
rowData: rowData,
onGridReady: function () {
gridOptions.api.sizeColumnsToFit();
}
};
// wait for the document to be loaded, otherwise
// ag-Grid will not find the div in the document.
document.addEventListener("DOMContentLoaded", function () {
console.log('before create grid');
// lookup the container we want the Grid to use
var eGridDiv = document.querySelector('#myGrid');
console.log(eGridDiv);
// create the grid passing in the div to use together with the columns & data we want to use
new agGrid.Grid(eGridDiv, gridOptions);
});
</script>
Monday, February 10, 2020
Some Other Great Asp.Net Core Tutorials in 2020
In the last weeks, I have been looking to further materials and information related to my main subject of study, Asp.Net Core. Even if my main projects are in .Net WPF, I really like the Asp.Net web world, and that's where I am deepening my knowledge.
Before going through the latest tutorials, the material I purchased from the code-maze guys has to be mentioned Asp.Net Core Web API course. This is the most serious course material I probably purchased, but easy to follow, with full support and explanations. I really prefer this approach vs Udemy or Youtube. I like to have a pdf file with explanations, code snippets and code repo, versus the lengthy youtube videos from any trainer, even if they are free.
In addition to the above, lets's see what I have found and studied (all of them, but not very deeply) in the last couple of weeks:
1. The blog of Saineshwar Bager, tutexchange.com
a)
https://tutexchange.com/creating-grid-view-in-asp-net-core-with-custom-searching/
This one contains a tutorial, how to present tabular data with paging, search and sorting data with asp.net core mvc. Really nice and useful one. Medium difficulty.
b)
https://tutexchange.com/how-to-upload-files-and-save-in-database-in-asp-net-core-mvc/
How to upload file content to a MS SQL database, some new idea, maybe I will further this idea on a later project. Really good quality one, as a), so I can only recommend his blog, which is a true delight for Asp.Net Core learners.
2. Another great tutorial source https://dotnetthoughts.net
c) Even if I prepared a Chart.JS tutorial in Dec 2019, this is a great one too, with Google Charts, see
https://dotnetthoughts.net/integrating-google-charts-in-aspnet-core/
d) From the same source, how to deploy an asp.net core app on Heroku:
https://dotnetthoughts.net/hosting-aspnet-core-on-heroku/
Until now I published my small works on Interserver.net, but this time I published my c) and d) learning directly on heroku! :)
https://dotnet-core-chart.herokuapp.com/
3. I always wanted to check the Angular client generation with NSWAG, so I tested this one out:
e) https://elanderson.net/2019/12/using-nswag-to-generate-angular-client-for-an-asp-net-core-3-api/
I wish the explanations were clearer on this site!
4. Also tried this one, Angular Chat app with SignalR
f) https://morioh.com/p/70381fe201a4?f=5c21f93bc16e2556b555ab2f&fbclid=IwAR0i4C9Mw9tfYJKWjTjoEuV8ngIN_lV3oxUevCFlyYXh3IT36NYQzXTjy1I
The idea is great, but the tutorial explanations are a bit superficial.
What would be your additions to the asp.net core tutorial world?
Before going through the latest tutorials, the material I purchased from the code-maze guys has to be mentioned Asp.Net Core Web API course. This is the most serious course material I probably purchased, but easy to follow, with full support and explanations. I really prefer this approach vs Udemy or Youtube. I like to have a pdf file with explanations, code snippets and code repo, versus the lengthy youtube videos from any trainer, even if they are free.
In addition to the above, lets's see what I have found and studied (all of them, but not very deeply) in the last couple of weeks:
1. The blog of Saineshwar Bager, tutexchange.com
a)
https://tutexchange.com/creating-grid-view-in-asp-net-core-with-custom-searching/
This one contains a tutorial, how to present tabular data with paging, search and sorting data with asp.net core mvc. Really nice and useful one. Medium difficulty.
b)
https://tutexchange.com/how-to-upload-files-and-save-in-database-in-asp-net-core-mvc/
How to upload file content to a MS SQL database, some new idea, maybe I will further this idea on a later project. Really good quality one, as a), so I can only recommend his blog, which is a true delight for Asp.Net Core learners.
2. Another great tutorial source https://dotnetthoughts.net
c) Even if I prepared a Chart.JS tutorial in Dec 2019, this is a great one too, with Google Charts, see
https://dotnetthoughts.net/integrating-google-charts-in-aspnet-core/
d) From the same source, how to deploy an asp.net core app on Heroku:
https://dotnetthoughts.net/hosting-aspnet-core-on-heroku/
Until now I published my small works on Interserver.net, but this time I published my c) and d) learning directly on heroku! :)
https://dotnet-core-chart.herokuapp.com/
3. I always wanted to check the Angular client generation with NSWAG, so I tested this one out:
e) https://elanderson.net/2019/12/using-nswag-to-generate-angular-client-for-an-asp-net-core-3-api/
I wish the explanations were clearer on this site!
4. Also tried this one, Angular Chat app with SignalR
f) https://morioh.com/p/70381fe201a4?f=5c21f93bc16e2556b555ab2f&fbclid=IwAR0i4C9Mw9tfYJKWjTjoEuV8ngIN_lV3oxUevCFlyYXh3IT36NYQzXTjy1I
The idea is great, but the tutorial explanations are a bit superficial.
What would be your additions to the asp.net core tutorial world?
Monday, February 3, 2020
AG-Grid Tutorial With Asp.Net Core Razor Pages - Part 2
Continuing the AG Grid series, I added some other features to my previous post. I cleared the repo a little bit, removing some unnecessary files and dependencies, and also adding the ag-grid specific js and css files to the local folder, instead of accessing them from the internet. So, I apologize because of the quality of the previous code.
My observation until now is that DataTables.js as a free JavaScript grid is an easier solution versus Ag-Grid. Later on I will try some enterprise features of Ag-Grid, maybe this will compensate for that.
Preliminary steps:
1. check the previous tutorial: https://mydev-journey.blogspot.com/2020/01/ag-grid-tutorial-with-aspnet-core-razor.html
1. application is live under: https://ag-grid2.zoltanhalasz.net/
2. zipped(cleaned) repo of the application: https://drive.google.com/open?id=10A0as_DTC94ve_oVtDF2uFW1cX19k4J7
How the app will look like:
The steps of the tutorial are:
1. please remove the unnecessary files from the wwwroot (if you are using the part 1 repo).
2. check their official community resource from github, cloning their repo https://github.com/ag-grid/ag-grid/tree/master/community-modules/all-modules/dist
a. take the file ag-grid-community.min.js and copy it under wwwroot/js folder
(or ag-grid-community.noStyle.js)
b. the same, for the CSS files, taken from the Styles folder in the repo above
copy the them in the Css folder, and then reference them from the Layout file:
<link rel="stylesheet" href="~/css/ag-grid.css">
<link rel="stylesheet" href="~/css/ag-theme-balham.css">
3. the whole source of the index page will be:
<script src="~/js/ag-grid-community.min.js"></script>
<h1>Hello from ag-grid - Part 2</h1>
<div>
<div id="mybutton">
<button onclick="onExportCsv()" class="btn-info">Export Csv</button>
</div>
<div id="myGrid" style="height: 450px;width:900px;" class="ag-theme-balham"></div>
</div>
<script type="text/javascript" charset="utf-8">
var deleteRenderer = function(params) {
var eDiv = document.createElement('div');
eDiv.innerHTML = '<span class="my-css-class"><button class="btn-del btn-danger btn-xs">Delete</button></span>';
var eButton = eDiv.querySelectorAll('.btn-del')[0];
eButton.addEventListener('click', function() {
console.log('will be deleted, invoice no:', params.data.ID);
window.location.href = './Delete?id='+params.data.ID;
});
return eDiv;
}
var editRenderer = function (params) {
var eDiv = document.createElement('div');
eDiv.innerHTML = '<span class="my-css-class"><button class="btn-edit btn-warning btn-xs">Edit</button></span>';
var eButton = eDiv.querySelectorAll('.btn-edit')[0];
eButton.addEventListener('click', function() {
console.log('will be edited, invoice no:', params.data.ID);
window.location.href = './Edit?id='+params.data.ID;
});
return eDiv;
}
var detailsRenderer = function (params) {
var eDiv = document.createElement('div');
eDiv.innerHTML = '<span class="my-css-class"><button class="btn-details btn-info btn-xs">Details</button></span>';
var eButton = eDiv.querySelectorAll('.btn-details')[0];
eButton.addEventListener('click', function() {
console.log('will be info, invoice no:', params.data.ID);
window.location.href = './Detail?id='+params.data.ID;
});
return eDiv;
}
// specify the columns
var columnDefs = [
{ headerName: "InvoiceNumber", field: "InvoiceNumber" },
{ headerName: "Amount", field: "Amount" },
{ headerName: "CostCategory", field: "CostCategory" },
{ headerName: "Period", field: "Period" },
{ headerName: "Delete", field: null, cellRenderer: deleteRenderer },
{ headerName: "Edit", field: null, cellRenderer: editRenderer },
{ headerName: "Details", field: null, cellRenderer: detailsRenderer },
];
// let the grid know which columns to use
var gridOptions = {
columnDefs: columnDefs,
defaultColDef: {
sortable: true,
filter: true,
width: 120,
},
rowHeight : 35,
pagination: true,
paginationPageSize: 10,
};
// lookup the container we want the Grid to use
var eGridDiv = document.querySelector('#myGrid');
function getParams() {
return {
suppressQuotes: null,
columnSeparator: null,
customHeader: null,
customFooter: null
};
}
function onExportCsv() {
var params = getParams();
gridOptions.api.exportDataAsCsv(params);
}
// create the grid passing in the div to use together with the columns & data we want to use
new agGrid.Grid(eGridDiv, gridOptions);
agGrid.simpleHttpRequest({ url: './Index?handler=ArrayData' }).then(function (data) {
gridOptions.api.setRowData(data);
});
</script>
My observation until now is that DataTables.js as a free JavaScript grid is an easier solution versus Ag-Grid. Later on I will try some enterprise features of Ag-Grid, maybe this will compensate for that.
Preliminary steps:
1. check the previous tutorial: https://mydev-journey.blogspot.com/2020/01/ag-grid-tutorial-with-aspnet-core-razor.html
1. application is live under: https://ag-grid2.zoltanhalasz.net/
2. zipped(cleaned) repo of the application: https://drive.google.com/open?id=10A0as_DTC94ve_oVtDF2uFW1cX19k4J7
How the app will look like:
The steps of the tutorial are:
1. please remove the unnecessary files from the wwwroot (if you are using the part 1 repo).
2. check their official community resource from github, cloning their repo https://github.com/ag-grid/ag-grid/tree/master/community-modules/all-modules/dist
a. take the file ag-grid-community.min.js and copy it under wwwroot/js folder
(or ag-grid-community.noStyle.js)
b. the same, for the CSS files, taken from the Styles folder in the repo above
copy the them in the Css folder, and then reference them from the Layout file:
<link rel="stylesheet" href="~/css/ag-grid.css">
<link rel="stylesheet" href="~/css/ag-theme-balham.css">
3. the whole source of the index page will be:
<script src="~/js/ag-grid-community.min.js"></script>
<h1>Hello from ag-grid - Part 2</h1>
<div>
<div id="mybutton">
<button onclick="onExportCsv()" class="btn-info">Export Csv</button>
</div>
<div id="myGrid" style="height: 450px;width:900px;" class="ag-theme-balham"></div>
</div>
<script type="text/javascript" charset="utf-8">
var deleteRenderer = function(params) {
var eDiv = document.createElement('div');
eDiv.innerHTML = '<span class="my-css-class"><button class="btn-del btn-danger btn-xs">Delete</button></span>';
var eButton = eDiv.querySelectorAll('.btn-del')[0];
eButton.addEventListener('click', function() {
console.log('will be deleted, invoice no:', params.data.ID);
window.location.href = './Delete?id='+params.data.ID;
});
return eDiv;
}
var editRenderer = function (params) {
var eDiv = document.createElement('div');
eDiv.innerHTML = '<span class="my-css-class"><button class="btn-edit btn-warning btn-xs">Edit</button></span>';
var eButton = eDiv.querySelectorAll('.btn-edit')[0];
eButton.addEventListener('click', function() {
console.log('will be edited, invoice no:', params.data.ID);
window.location.href = './Edit?id='+params.data.ID;
});
return eDiv;
}
var detailsRenderer = function (params) {
var eDiv = document.createElement('div');
eDiv.innerHTML = '<span class="my-css-class"><button class="btn-details btn-info btn-xs">Details</button></span>';
var eButton = eDiv.querySelectorAll('.btn-details')[0];
eButton.addEventListener('click', function() {
console.log('will be info, invoice no:', params.data.ID);
window.location.href = './Detail?id='+params.data.ID;
});
return eDiv;
}
// specify the columns
var columnDefs = [
{ headerName: "InvoiceNumber", field: "InvoiceNumber" },
{ headerName: "Amount", field: "Amount" },
{ headerName: "CostCategory", field: "CostCategory" },
{ headerName: "Period", field: "Period" },
{ headerName: "Delete", field: null, cellRenderer: deleteRenderer },
{ headerName: "Edit", field: null, cellRenderer: editRenderer },
{ headerName: "Details", field: null, cellRenderer: detailsRenderer },
];
// let the grid know which columns to use
var gridOptions = {
columnDefs: columnDefs,
defaultColDef: {
sortable: true,
filter: true,
width: 120,
},
rowHeight : 35,
pagination: true,
paginationPageSize: 10,
};
// lookup the container we want the Grid to use
var eGridDiv = document.querySelector('#myGrid');
function getParams() {
return {
suppressQuotes: null,
columnSeparator: null,
customHeader: null,
customFooter: null
};
}
function onExportCsv() {
var params = getParams();
gridOptions.api.exportDataAsCsv(params);
}
// create the grid passing in the div to use together with the columns & data we want to use
new agGrid.Grid(eGridDiv, gridOptions);
agGrid.simpleHttpRequest({ url: './Index?handler=ArrayData' }).then(function (data) {
gridOptions.api.setRowData(data);
});
</script>
5. pagination
- this is solved using the gridOptions object, setting the properties:
pagination: true,
paginationPageSize: 10,
- this is solved using the gridOptions object, setting the properties:
pagination: true,
paginationPageSize: 10,
6. Csv export (basic)
- this is managed using the button with id = "mybutton", and writing its onclick event function:
function onExportCsv() {
var params = getParams();
gridOptions.api.exportDataAsCsv(params);
}
for the params object, I returned the basic features for a csv export using getParams function.
See more detailed explanation about csv export on their page: https://www.ag-grid.com/javascript-grid-csv/
- this is managed using the button with id = "mybutton", and writing its onclick event function:
function onExportCsv() {
var params = getParams();
gridOptions.api.exportDataAsCsv(params);
}
for the params object, I returned the basic features for a csv export using getParams function.
See more detailed explanation about csv export on their page: https://www.ag-grid.com/javascript-grid-csv/
7. height and width on the rows/columns
width: 120, // in defaultColDef properties
rowHeight: 35, // in gridOptions properties
width: 120, // in defaultColDef properties
rowHeight: 35, // in gridOptions properties
8. cell rendering (basic)
for example, in the columnDef for Delete
{ headerName: "Delete", field: null, cellRenderer: deleteRenderer },
deleteRenderer will be a function generating html for the delete button
var deleteRenderer = function(params) {
var eDiv = document.createElement('div');
eDiv.innerHTML = '<span class="my-css-class"><button class="btn-del btn-danger btn-xs">Delete</button></span>';
var eButton = eDiv.querySelectorAll('.btn-del')[0];
eButton.addEventListener('click', function() {
console.log('will be deleted, invoice no:', params.data.ID);
window.location.href = './Delete?id='+params.data.ID;
});
return eDiv;
}
for example, in the columnDef for Delete
{ headerName: "Delete", field: null, cellRenderer: deleteRenderer },
deleteRenderer will be a function generating html for the delete button
var deleteRenderer = function(params) {
var eDiv = document.createElement('div');
eDiv.innerHTML = '<span class="my-css-class"><button class="btn-del btn-danger btn-xs">Delete</button></span>';
var eButton = eDiv.querySelectorAll('.btn-del')[0];
eButton.addEventListener('click', function() {
console.log('will be deleted, invoice no:', params.data.ID);
window.location.href = './Delete?id='+params.data.ID;
});
return eDiv;
}
9. Detail, Edit, Delete Razor pages added for complete CRUD, as per cell rendering links suggest.
This is achieved using Entity Framework applied on the In-Memory database.
This is achieved using Entity Framework applied on the In-Memory database.
Subscribe to:
Posts (Atom)