Printing Web Pages – the Cool Way!

When developing web applications we often run into a request to print one or more pages within the applications. In most cases, these pages will contain some type of report.
There are tons of ways to do that. Most of them I don’t like, especially the trivial one that pops to mind - “open a new window with a printer-friendly version of the page and set onload=’window.print()‘”. I find it quite irritating for the user.

There is another solution, which is less popular than its fellow window.open solution, but IMHO is much more user friendly and also might save you a few lines of code. It’s the CSS @media print solution!

What’s it all about?

  1. You get to have a single page that prints in slightly a different way than it’s presented on the browser.
  2. The magic lies within a special CSS block where you define CSS classes that will be used when the page is printed.
  3. This way you can hide parts of the page that don’t need to be printed, change the background image, increase/decrease font size, etc.

 

How to use it

The idea is to declare a different set of CSS styles for the page’s print mode. The great thing about that is that you can also, via CSS, hide elements that are unnecessary for printing.

For example, assuming you have the next HTML document:

<html>
<head>
    <title></title>
</head>
<body>
  <h1>REPORT</h1>
  <form><input type="button" id="printButton" value="Print" onclick="window.print()"/></form>
  <table border="1">
    <thead>
      <tr>
        <td>Column A</td><td>Column B</td><td>Column C</td>
      </tr>
    </thead>
    <tbody>
      <tr><td>1</td><td>2</td><td>3</td></tr>
      <tr><td>4</td><td>5</td><td>6</td></tr>
      <tr><td>7</td><td>8</td><td>9</td></tr>
    </tbody>
  </table> 
</body>
</html>
  1. This page will be presented as follows:
  2. image

Great UI indeed. However, printing this page will be a bit ugly (surprise surprise!), but really, in terms of printing the page, the header shouldn’t be that big and the print button shouldn’t be visible at all.

To do so, all we have to do is to define the styles for printing mode:

<style type="text/css">
  @media print
  {
    /* Here goes all styles to be used when printing */

    #printButton { display: none; } /* hide the print button */
    h1 { font-size: 12px; } /* make the header 12 pixels */
    body, td { 10px; } /* make regular text 10 pixels */
  }
</style>  

That’s it. Now if we print-preview the page, the header will be smaller and the Print button will be hidden:

Print Preview with @media print

Voila! No window.open is needed!

[Side note: I know this approach might be problematic sometimes because the user wouldn’t know what to expect when the page is printed. However, I still think this is a much more elegant way than the window.open approach. Use it wisely though].

ASP.NET developers: be careful when using CSS’s ‘#’ symbol with element IDs. Remember that ASP.NET changes the final HTML element ID of your controls that have the runat=”server” attribute.

All the best,
Shay.

Full source:

<html>
<head>
  <title></title>
  <style type="text/css">
    @media print
    {
      /* Here goes all styles to be used when printing */

      #printButton { display: none; } /* hide the print button */
      h1 { font-size: 12px; } /* make the header 12 pixels */
      body, td { 10px; } /* make regular text 10 pixels */
    }
  </style>  
</head>
<body>
  <h1>REPORT</h1>
  <form><input type="button" id="printButton" value="Print" onclick="window.print()"/></form>
  <table border="1">
    <thead>
      <tr>
        <td>Column A</td><td>Column B</td><td>Column C</td>
      </tr>
    </thead>
    <tbody>
      <tr><td>1</td><td>2</td><td>3</td></tr>
      <tr><td>4</td><td>5</td><td>6</td></tr>
      <tr><td>7</td><td>8</td><td>9</td></tr>
    </tbody>
  </table> 
</body>
</html>


Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

Announcement: IronRuby Unleashed has been Released!

Good times!

Right after IronRuby RC2 was released and a bit before IronRuby 1.0 RTM is out, my book, IronRuby Unleashed, is available!

The print book is available from today on the InformIT web site (Sams Publishing’s book store) and will be available shortly on Amazon as well (and other book stores too).

IronRuby Unleashed by Shay Friedman

In short, IronRuby Unleashed contains all you need to start developing IronRuby applications. From Ruby language introduction, through the fundamentals of IronRuby programming (like implementing CLR interfaces, using generic methods, overriding CLR events and more) to using IronRuby with several different development frameworks like WPF, ASP.NET MVC, Silverlight, Ruby on Rails and more. The book also includes some advanced IronRuby topics like a chapter that explains how to develop IronRuby extensions.

 

I’d be glad to hear comments about the book, so don’t hesitate to contact me!

Shay.



Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

Use .NET Built-in Methods to Save Time and Headaches

During our everyday programming tasks we run into several repetitive code blocks that after the 20th time you implement them become really annoying. The worst case is to re-implement these code blocks every time, and the better case is to create a central class library with helper classes and methods. However, a large amount of these tasks can be achieved easily with built-in .NET methods.

In this post I will go through several repetitive code blocks and show you how to implement them using built-in .NET method. If you want to add your suggestions, comment! I’ll add your suggestions to the post periodically.

Disclaimer: I’m sure some of the code blocks I use in the NOT Recommended sections can be written much better. These code blocks are here just for demonstration purposes.

Code Block #1 – Check string for nullity or emptiness

NOT Recommended

str = "something"
if (str == null || str == String.Empty)
{
	// Oh no! the string isn't valid!
}

Recommended

str = "something"
if (String.IsNullOrEmpty(str))
{
	// Oh no! the string isn't valid!
}

Code Block #2 – Check string for nullity or emptiness (spaces only string is invalid too)

NOT Recommended

str = "something"
if (str == null || str.Trim() == String.Empty)
{
	// Oh no! the string isn't valid!
}

Recommended (C# 4.0 Only)

str = "something"
if (String.IsNullOrWhiteSpace(str))
{
	// Oh no! the string isn't valid!
}

Code Block #3 – Copy an Array

NOT Recommended

string[] source = new string[] { "a", "b", "c" };
string[] dest = new string[3];
for (int i=0; i < source.Length; i++)
{
	dest[i] = source[i];
}

Recommended

string[] source = new string[] { "a", "b", "c" };
string[] dest = new string[3];
Array.Copy(surce, dest, source.Length);

Code Block #4 – Check if a char is a digit

NOT Recommended

char c = '1';
if (c == '1' || c == '2' || c == '3' ||
	c == '4' || c == '5' || c == '6' ||
	c == '7' || c == '8' || c == '9' ||
	c == '0')
{
	// It's a digit!
}

Recommended

char c = '1';
if (Char.IsDigit(c))
{
	// It's a digit!
}

Code Block #5 – Combine Paths

NOT Recommended

string folder = @"C:\MyDir";
string file = "MyFile.docx";
// Combine to make a path
string path = folder + @"\" + file;

Recommended

string folder = @"C:\MyDir";
string file = "MyFile.docx";
// Combine
string path = System.IO.Path.Combine(folder, file);

Code Block #6 – Get file extension out of a file path

NOT Recommended

string path = @"C:\MyDir\MyFile.docx";
string extension = path.Substring(path.LastIndexOf("."));

Recommended

string path = @"C:\MyDir\MyFile.docx";
string extension = System.IO.Path.GetExtension(path);

Code Block #7 – Get MyDocuments Path

NOT Recommended

// Probably some nasty stuff here

Recommended

Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);

Code Block #8 – Check if object is of a specific type

NOT Recommended

object obj = "str";
if (obj.GetType() == typeof(String))
{
	// It's a string!
}

Recommended

object obj = "str";
if (obj is String)
{
	// It's a string!
}

As Adrian Aisemberg has pointed out, these samples are not entirely the same. The is keyword will return true also if obj is of a derivative type of String (in this sample).

Code Block #9 – Set default enum value

NOT Recommended

public class MyClass
{
	private enum Sample 
	{
		A,
		B,
		C
	}
	static Sample s = Sample.B; // Set default value explicitly
	public static void Run()
	{	
		Console.WriteLine(s); // Prints B
	}
}

Recommended

public class MyClass
{
	private enum Sample 
	{
		A,
		B = 0, // Make B the default value
		C
	}
	static Sample s; // Default value will be used
	public static void Run()
	{	
		Console.WriteLine(s); // Prints B
	}
}

Code Block #10 – Check if a string starts with another string

NOT Recommended

string str = "Hello World";
if (str.Substring(0, 5) == "Hello")
{
	// String starts with Hello!			
}

Recommended

string str = "Hello World";
if (str.StartsWith("Hello"))
{
	// String starts with Hello!		
}

Code Block #11 – Convert list of items of one type to a list of items of a different type

NOT Recommended

List<int> list = new List<int>(new[] { 1, 2, 3, 4, 5 });
List<string> convertedList = new List<string>();
foreach (int item in list)
{
	convertedList.Add(item.ToString());
}

Recommended

List<int> list = new List<int>(new[] { 1, 2, 3, 4, 5 });
List<string> convertedList = list.ConvertAll<string>(Convert.ToString);

Code Block #12 – Check if a string contains a number and get the number

NOT Recommended

string str = "4";

int num = 0;
bool success = false;
try 
{
	num = Convert.ToInt32(str);
	success = true;
}
catch
{
	success = false;
}

if (success)
{
	// Do something with the number
}

Recommended

string str = "4";

int num = 0;
if (Int32.TryParse(str, out num))
{
	// Do something with the number
}

Code Block #13 – Writing a string to a file (courtesy of Yaron Naveh)

NOT Recommended

const string str = "put me in a file";
const string file = @"c:\logs\file.txt";

var fs = new FileStream(file, FileMode.Create);          
var sw = new StreamWriter(fs);
sw.Write(str);

sw.Close();
fs.Close();

Recommended

const string str = "put me in a file";
const string file = @"c:\logs\file.txt";

File.WriteAllText(file, str);

Code Block #14 – Pick value if not null and a different on if it is (courtesy of Abhishek)

NOT Recommended

string input = "sdfds";
string result = null;
if (input == null)
{
	result = "Input is null!";
}
else
{
	result = input;
}

Recommended

string input = "sdfds";
string result = input ?? "Input is null!";

 

This is it for now. If you have more, comment and I’ll add your suggestions to the list (with credits).

All the best,
Shay.

kick it on DotNetKicks.com Shout it


Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

Vote to See IronRuby in MIX10

Do you want to hear about IronRuby in the upcoming MIX10 conference? If so, make sure to vote for my IronRuby session - IronRuby - the Development Booster Machine.

In the session I plan to talk a bit about the Ruby language and its strengths and then move on and show how .NET developers can take advantage of these stengths in several different scenarios like testing, debugging and Silverlight.

YOU have the power to make it happen! Go ahead and vote!
http://visitmix.com/opencallvote/Entry?entryId=IRONRU127

See you all there,
Shay.

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

A New Year, A New Beginning

2010!

It’s a new year and the wind of change is in the air. This year is a big year for me – IronRuby Unleashed will be published (est. FEB 15th), another big project is coming (more details in a few weeks) and I changed jobs!

In the last 4 years I worked at a startup named ActionBase. I learned a lot at this place and managed to work with various platforms – Office addins, WinForms, Silverlight and ASP.NET. After 4 years it was a time for a change and I decided to move on. Starting from 1/1/2010 I’m a .NET technologies consultant and instructor at Sela. My main field is dynamic languages but I’ll be doing other stuff too. I’m really thrilled and excited to get started, help customers and spread my knowledge.

If you want me to come to your company to run a course or consult, don’t hesitate to contact me. This is my job now, not a hobby anymore, so go ahead and use me! :)

I wish you all a wonderful and happy new year. May this year will be the one you were waiting and hoping for!

Peace and love,
Shay.



Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

Good to Know: Built-in ASP.NET Http Handlers

Http handlers is a really elegant infrastructure allowing you to provide special behavior to specific paths in your application. You can use them to create RSS  feeds, dynamically generate images, handle requests differently and  more.

Apart from writing new http handlers, there are some built-in handlers that you can take advantage of. The built in handlers are separated into three categories (in my opinion at least): request handlers, error generators and misc. handlers.

Request Handlers

These handlers process requests in a specific way. Although these already have paths configured  for them, you can manually configure them in the web.config (or via  IIS Manager) to run on different  paths. For example, treat ABC files the same as ASPX files. There is one catch here – because ASPX, ASHX and ASMX files are dynamically compiles, you will also need to provide build configurations for the new extensions.

The handlers in this category include:

  • The regular ASP.NET page (ASPX) hander - System.Web.UI.PageHandlerFactory.
  • The generic handler (ASHX) handler - System.Web.UI.SimpleHandlerFactory.
  • The resource handler (WebResource.axd) - System.Web.Handlers.AssemblyResourceLoader.
  • The web service handler (ASMX) - System.Web.Services.Protocols.WebServiceHandlerFactory.
  • The trace handler (trace.axd) – System.Web.Handlers.TraceHandler.

The following web.config  sample enables accessing trace info via MyTrace.aaa (this is web.config configuration for IIS 7 running in Integrated  mode, in other versions or modes the configuration will be a bit different):

    <system.webServer>
      <handlers>
        <add verb="*" path="MyTrace.aaa" name="Trace-handler" type="System.Web.Handlers.TraceHandler"/>
      </handlers>
    </system.webServer>

After this is configured (and tracing is enabled as well), try navigate to MyTrace.aaa page.

    Error Generators

    These handlers generate specific http errors. You can use them to prevent access to specific files, specific extensions or folders. These error generator handlers include:

  • Forbidden – generates a 403 Forbidden http error - System.Web.HttpForbiddenHandler.
  • Not Found – generates a 404 Not Found  http error - System.Web.HttpNotFoundHandler.
  • Method Not Allowed – generates a 405 Method Not Allowed http error - System.Web.HttpMethodNotAllowedHandler.
  • Not Implemented – generates a 501 Not Implemented http error - System.Web.HttpNotImplementedHandler.

The following web.config sample prevents users from accessing all files with secret extensions. Once a user tries to access such file he or she will get a 403 Forbidden page:

<system.webServer>
  <handers>
    <add verb="*" path="*.secret" name="SecretAccess" type="System.Web.HttpForbiddenHandler"/>
  </handlers>
</system.webServer>

Try navigating to top.secret and witness the result.

    Miscellaneous

    The misc. category contains one handler, the static file handler, which can help in various  scenarios. It will present the file content without any processing. With this handler you can, for example, enable users to retrieve lkr file (no special meaning to this extension that I know of) content.

  • Static file – shows the content of a file without any processing - System.Web.StaticFileHandler.

The following  sample configures lkr files to be processed by the static file handler. To test this, create a txt file in your web application root folder, rename its extension to lkr and navigate to it.

<system.webServer>
  <handlers>
    <add verb="*" path="*.lkr" name="lkr-handler" 
              type="System.Web.StaticFileHandler" resourceType="File"/>
  </handlers>
</system.webServer>

 

All  the best,
Shay.

Share it:
Shout it kick it on DotNetKicks.com



Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

We’re Living in an Unstandardized World

This post has started from my suffering from developing web sites differently to make them work in IE and in Firefox the same. That was everything I asked for…Currently, even with IE8 I still can’t develop on Firefox and remain calm and confident that the site works the same on IE.

The truth is that it is not entirely Microsoft’s fault. When they begun developing IE somewhere in 1995, standards were not that important – it was more about “please just work” kind of work. In addition, even if there were standards, we can assume that the guys at  Microsoft assumed that they would change the standards as they had done before in other fields. What Microsoft didn’t realize was that the Internet was stronger and that they were not going to win the standards battle this time.

Anyway, out of my suffering I came to a conclusion – even though we like to think of Microsoft as this big evil doer that doesn’t follow standards, it is just another brick in the wall of global un-standardization that started long long ago and still effects our everyday life.

Language – yes, language. Starting from the beginning of days, the very basic resource of our communication is a one big un-standard thing. Just visit the country near you (and sometimes even the district near you) and most likely you will run into a different language than yours. Think of how big it is – almost everything you create in one country, should be fixed in some way to fit a second country. TV shows, manuals, books, user interfaces, etc.

If you do not agree with me because language is related to culture and different cultures is a part of our nature (some would say that it’s a kind of un-standardization, too), the next bullet will be harder for you to disprove.

Signs – especially driving signs. This is something which is entirely unrelated to the local culture. We all should stop at the traffic light, slow when entering school perimeter and beware of rolling stones. As a result, driving rules across the globe are very similar indeed. However, every country uses a slightly different sign set. For example, look at the different Stop signs from across the globe:

Stop in the USAOld UK Stop SignIsraeli Stop SignStop sign in Zimbabwe

If we had to develop a globalized application that involves driving signs, we had to create local versions for each country – very similar to writing code for IE and for Firefox…

Driving direction – that always amazes me. The world is split to about 34% live in right hand traffic countries and 66% live in left hand traffic countries (according to Wikipedia). This is such a big historical standard failure! it forces car companies to produce different versions of cars to meet both standards!

Left hand car Right hand car
In London it’s funny to see drivers from other EU countries driving their right-sided cars and trying to understand how to enter the roundabout. Actually it’s not that funny if they enter the roundabout in the wrong direction – life can be lost! and it’s all because of un-standardization…

The IE vs. FF issue seems less important now, doesn’t it?

Electricitythe differences in electricity methods around the world is astonishing – 110W, 220W, plug with 2 holes, with 3 holes, with wide holes, with thin holes…… huh? why?

 Australia/New ZealandUKIndiaUSAItalyIsrael

It’s like every country developed electricity by itself and didn’t tell the others until they developed it as well. do I hear someone say “Microsoft  and Netscape!”?

In conclusion, our world is full of un-standardization – from the language we talk to Internet development. There are much more examples than the ones I’ve brought here – shoe and shirt sizes, Km and Miles, Kg and Pounds, Meters and Feet, money and more. Although it might be upsetting to meet all standards it also has one big advantage – it creates so many jobs! consequently it helps the world’s economy!

So Microsoft actually helps the world. Yes.

All the best,
Shay.



Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

Executing IronPython Code from IronRuby

One of the advantages of the Dynamic Language Runtime (DLR) is the fact that it makes sharing code between the languages that are written on top of it (and on top of the CLR as well). Therefore, it is possible to share code between IronPython and IronRuby (and any other DLR language as well like IronScheme).

This means that IronPython libraries can be used from IronRuby code and vice versa. Ruby on Rails in Python? Django in Ruby? feels like the end of days, isn’t it? perhaps we should really start preparing to year 2012

In this post I’ll show you how to run simple IronPython code from IronRuby so you can take it and do whatever your imagination guides you to.

Assuming we have a demo.py IronPython file with the next content:

class MyPythonClass:
  def add(self, x, y):
    return x + y

welcome_message = "Hello from Python!"

 

To those of you who don’t know Python, the code  above declares a class named MyPythonClass with a single method named add that combines two numbers and return the result. It also contains a variable named welcome_message.

Note

Because executing Python code from IronRuby involves DLR services, methods that you call from IronRuby must have self as their first argument just like IronRuby-targeted C# methods have (if the method accepts no parameters, then it should have only the self argument). This argument contains the caller class instance.
This means that python code that should be executed by IronRuby should be modified to match the requirements.

After we have the python file in place we can use it from IronRuby. The key for doing so is loading the python file using the IronRuby.require method. This method is similar to the Kernel#require method but with a small difference – it returns the DLR scope object of the loaded script.  This enables you to call the script members via the scope, just like when you load a script manually via the DLR LoadFile method.

The next IronRuby is pretty straight-forward:

# Load the python file
python = IronRuby.require('demo.py')

# Get an instance of MyPythonClass
python_class = python.MyPythonClass()
# Execute the add method (pay attention that there's no 
# need to pass the self parameter, this is done automatically)
puts python_class.add(1, 5)

# Get the python variable and print its value
puts python.welcome_message
# Set the python variable
python.welcome_message = "Hello from Ruby!"
# Print its new value
puts python.welcome_message

 

The output is:
6
Hello from Python!
Hello from Ruby!

Note that for this sample to run, you need IronRuby and IronPython installed on your machine, both compiled with the same Microsoft.Scripting project. I just compiled IronRuby’s and IronPython’s sources to make  it work.

In conclusion, the DLR opens a bunch of new and exciting possibilities specifically in the field of code sharing between dynamic languages and dynamic and static languages. Go ahead and try it, it’s magical!

All the best,
Shay.

kick it on DotNetKicks.com Shout it



Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

IronRuby Sample #3: Creating a DSL

[ This is part 3 of my IronRuby samples series. You can read the first post (Hello World) and the second post (C# Recorder using IronRuby) as well. ]

The Ruby language is very powerful in general, and in its metaprogramming abilities in particular. This time I’m going to demonstrate how you can take advantage of these abilities along with Ruby’s syntax capabilities in order to create a custom DSL (Domain Specific Language) in a matter of minutes. Pay attention that these features are not specific to IronRuby, they are a part of every implementation of the Ruby language.

Let’s start from the end this time. This is the code you will be able to write with my little DSL:

list = ShoppingList.new
list.buy 3.kilograms.of(Peanuts)
list.buy 100.grams.of(Cheese)
list.buy 2.kilograms.of(Snacks)
list.print

Running this will result in the next output to the console:

Please buy:
- 3000 grams of peanuts
- 100 grams of cheese
- 2000 grams of snacks

It’s a nice little DSL to manage your shopping lists with. But as much nice as it is, the more important fact is that you can write your custom DSLs in Ruby (and IronRuby of course) with ease and make your coding experience much more fluent.

How is it Written?

The code for this DSL contains 49 lines of code, including lots of comments… Again, writing DSLs in Ruby (and IronRuby) is very easy. All I needed for this DSL were two “special” techniques of Ruby – const_missing and monkey patching. These two together makes about all the magic of this DSL.

I’m not going to go through the code line by line. I added comments inside the code so I think it will pretty straight forward to read. Do not hesitate to comment or contact me directly if something is not clear enough.

the DSL code:

# ShoppingList class
class ShoppingList
  # Ruby's class constructor - initializes the shopping list array.
  def initialize
    @list = []
  end
  
  # The main method - gets an item and adds it to the array.
  def buy(item)
    @list << item
  end
  
  # Prints all items in the list in a user-friendly way.
  def print
    puts "Please buy:"
    @list.each do |item|
      puts "- #{item[:grams]} grams of #{item[:product].downcase}"
    end
  end
end

# Every call to an undefined constant will reach here.
# This allows to use Peanuts inside the DSL code without defining it
# so we can write 5.grams.of(Peanuts) instead of 5.grams.of("Peanuts")
def Object.const_missing(name)
  # Return the constant as a string
  name.to_s
end

# For the DSL, we need to add grams and kilograms methods to all integers in the application.
# In order to do that, I open the Fixnum class (which is Ruby's equivalent to C# Int32) and add
# the needed methods
class Fixnum
  # grams method will just return itself
  def grams
    self
  end
  
  # Because we save everything in grams, when the kilograms method is used, I'll return 
  # the current value * 1000.
  def kilograms
    self*1000
  end
  
  # The of method retrieves an object and returns a hash of the object and its gram amount.
  def of(product)
    {:product=>product, :grams=>self}
  end
end


All the best,
Shay.



Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

Just Released: IronRuby 0.9.2

We’re getting closer to V1.0!

Version 0.9.2 fixes 44 bugs (some of the irritating ones like the Rails' bug with :default_url_options and the bug that prevented the RSS standard library from running).
The biggest announcement in the 0.9.2 version, in my opinion, is that IronRuby now has an MSI-based installer for Windows! no need to extract a zip file and do everything yourself anymore.

Download it from: http://ironruby.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=33693#DownloadId=90621
0.9.2 release notes: http://rubyforge.org/frs/shownotes.php?release_id=41087

Good times,
Shay.



Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

Subscribe Subscribe

That's Me!

Hi! I'm Shay Friedman
I'm Shay Friedman - an eager developer, author, speaker and new technologies freak
More about me

Contact Me

> Contact page
> Twitter: @ironshay
> LinkedIn profile

Search

Sponsers